Browse Source

G-100 achieved: suite_tcl86 dependency set (Tk 8.6, tklib, tcllib + tcllibc) and an 8.6 runtests census

Extends the 8.6 buildsuite with the remaining pieces punkshell's Tcl 8 testing
depends on, all built from source with the pinned zig 0.16.0 toolchain only.

Tk 8.6.17 (build_tk86/build_tk86.zig) is a RE-DERIVATION from the staged 8.6
win/makefile.vc + rules.vc, not a port of suite_tcl90's build_tk.zig:

  - no generic/tkPkgConfig.c in 8.6 -> no CFG_* defines at all
  - rules.vc TCL_MAJOR_VERSION==8 arm -> tk86t.dll + lib/tk8.6 (vs tcl9tk90.dll)
  - @TK_WIN_VERSION@ substitutes $(DOTVERSION).0.0, not the tcl-style
    <dotversion>.<releaselevel>.<patchlevel> formula
  - 8.6 TKOBJS carries no tkIcu/tkImgListFormat/tkImgSVGnano/tkWinGDI/tkWinIco/
    tkWinSysTray; XLIBOBJS is a first-class makefile macro here (the 9.0
    derivation had to recover those files by hand)

USE_TCL_STUBS + USE_TCLOO_STUBS against libtclstub86.a is the stock rules.vc
arrangement and keeps the dll loadable by both suite shells. tkMain.c is
compiled twice per makefile.vc, the ansi copy forced with -UUNICODE/-U_UNICODE
(verified the pinned zig cc defines neither for x86_64-windows-gnu, so the 8.6
core's ANSI/TCHAR arrangement is matched exactly).

tklib 0.9 and tcllib 2.0 install via their own installer.tcl (batch, packages
only, explicit -pkg-path so the installer's tcl_pkgPath-derived default can
never write off-tree); tcllibc is built by critcl 3.3.1 driving zig cc (tracked
critcl_zig.config, target pinned win32-x86_64-zig - critcl otherwise probes PATH
and falls back to its builtin msvc target). critcl compiles against its own
bundled headers chosen by the driving shell's version, which is why one config
serves both generations.

New evidence tools, run on every build:

  tools/tk_smoke.tcl     maps a real toplevel with classic + themed widgets,
                         updates, then destroys it - a load-only check passes
                         even with no usable display
  tools/accel_bench.tcl  same payload through sha1's critcl and pure-Tcl
                         implementations with the digests compared:
                         4MB -> critcl 5.7ms vs pure-tcl 2736ms = 477x

Test tiers (G-107 policy pattern): test-tcllib/test-tklib on the record tier
inside test-libraries, plus an opt-in test-tk (winDialog/winMsgbox excluded by
default - their message-injection automation blocks on a human under a batch
shell). First censuses on tclsh86ts.exe 8.6.18:

  thread     142 run /  0 failed   GATE PASS
  tklib      796 run /  0 failed   record
  tcllib   64259 run / 41 failed   record (names in the summary artifact)
  tclvfs      51 run /  4 failed   GATE PASS after baseline extension

The tclvfs baseline grew vfsFtp-1.1..1.3: they sit behind an 'ftp' constraint
needing tcllib's ftp package - absent at the G-099 capture, satisfied now that
the suite installs tcllib - so they run and reach the retired ftp.tcl.tk, the
same external-service class already baselined for vfsUrl-1.2/2.1.

punkshell runtests census (both runs 2026-07-26, identical flags, default
single-process sequential - 8.6 has no tcl::process so -jobs re-convoys):

  native Tcl 8.6.17   1003 total / 986 passed / 10 skipped / 7 failed
  suite tclsh86ts     1003 total / 987 passed / 10 skipped / 6 failed

runtests_parity.tcl finds exactly ONE difference: punkcheck
concurrency_two_writers_no_corruption failed in the BASELINE run and passed on
the suite runtime - a full-suite-load flake (6/6 twice in isolation on both
runtimes). Zero suite-runtime-only failures. Tk-dependent suites ran unskipped
(repl consolebackends 3/3, including the tkconsole backend on the suite Tk).

Two defects fixed in passing:

  - scrubTclEnv never cleared TCL<major>_<minor>_TM_PATH, so suite shells
    silently preferred machine/repo tm modules over what the suite had just
    installed (a repo-built sha1-2.0.4.tm shadowed tcllib's 2.0.5, md5 2.0.8
    shadowed 2.0.9) - any census through such a shell measures the machine. The
    scrub list now covers TCL8_0..8_7 + TCL9_0/9_1 in all three sites (per-step,
    suite.tcl process-level, bootstrap nested build). suite_tcl90 has the same
    exposure via TCL9_0_TM_PATH and is deliberately untouched here (out of this
    goal's scope) - pointers recorded in G-108 and G-116.
  - build.zig.zon's .fingerprint was hand-authored, so 'zig build' from the
    tracked suite dir died with 'invalid fingerprint' - the no-tclsh bootstrap
    flow could never have run. Corrected to zig's computed value (its low half
    is a checksum of the package name); 'zig build stage' now verified.

Goal flipped to achieved 2026-07-26 and archived; reference sweep updated G-005,
G-101, G-105 and ARCHITECTURE.md, and the archive-time xref pair G-101 <-> G-116
(whose only bridges were the archived G-100/G-103) gained a direct Related
pointer. No project-version bump - arm's-length build tooling, not shipped shell
behaviour.

Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
f89bd58c31
  1. 4
      ARCHITECTURE.md
  2. 4
      GOALS-archive.md
  3. 4
      GOALS.md
  4. 2
      goals/G-005-zig-build-infrastructure.md
  5. 57
      goals/G-100-suite-tcl86-tk-tcllib.md
  6. 18
      goals/G-101-tcl86-kit-container-strategy.md
  7. 5
      goals/G-105-buildsuite-cross-target.md
  8. 6
      goals/G-108-buildsuite-debug-tier.md
  9. 7
      goals/G-116-suite-built-tcltls.md
  10. 210
      goals/archive/G-100-suite-tcl86-tk-tcllib.md
  11. 13
      src/buildsuites/AGENTS.md
  12. 144
      src/buildsuites/suite_tcl86/README.md
  13. 48
      src/buildsuites/suite_tcl86/build.zig.zon
  14. 232
      src/buildsuites/suite_tcl86/build86.zig
  15. 64
      src/buildsuites/suite_tcl86/build_common.zig
  16. 260
      src/buildsuites/suite_tcl86/build_tk86/build_tk86.zig
  17. 36
      src/buildsuites/suite_tcl86/critcl_zig.config
  18. 23
      src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt
  19. 24
      src/buildsuites/suite_tcl86/sources.config
  20. 44
      src/buildsuites/suite_tcl86/suite.tcl
  21. 50
      src/buildsuites/suite_tcl86/tools/accel_bench.tcl
  22. 64
      src/buildsuites/suite_tcl86/tools/tk_smoke.tcl

4
ARCHITECTURE.md

@ -83,7 +83,7 @@ Two layers with a deliberate dependency direction (the class never depends on th
- **`src/make.tcl` is the single build entry.** Key subcommands: `modules`/`libs`/`packages` (repo-root build outputs), `vfscommonupdate` (regenerate `_vfscommon.vfs`), `bake` (kit assembly into `bin/`; optional kit names confine the run to those kits - G-121), `bakelist` (report the configured kit matrix with runtime/vfs presence and deployed state), `bin`, `bakehouse` (packages + bake for a clean checkout; `project`/`vfs` remain as deprecated aliases - G-112 rename, achieved), `bootsupport` (snapshot refresh), `vendorupdate`, `check`, `workflow`, `projectversion`. Dispatch and help dogfood `punk::args` (G-030, achieved) and degrade to plain scan/help when bootsupport `punk::args` is stale (`PUNKBOOT_PLAIN=1` forces the degraded mode). Source: `src/AGENTS.md`.
- **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.
- **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, 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 companions, gated against the 8.6 core/thread/tclvfs testsuites. The 8.6 dependency set (Tk/tcllib) and kit container strategy remain in-flux - see "In-flux areas".
- **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<major>_<minor>_TM_PATH` as well as TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY - without it a census measures the machine's module trees. The 8.6 kit container strategy remains in-flux - see "In-flux areas".
- **Project generation.** `dev project.new` composes thin layouts from `src/project_layouts/` (overlay chain with `.anti` deletion markers and `name@base` derivation - G-087, achieved) and injects bootsupport modules from the generating shell at generation time.
- **Workflow overview.** `tclsh src/make.tcl workflow` prints the embedded ASCII data-flow overview of the build/release pipeline, with its own update contract in `src/AGENTS.md`. This section deliberately summarises rather than copies it.
@ -103,7 +103,7 @@ Mechanics here are documented as pointers only - detailed prose would be obsolet
- Stored configuration and help: G-014 (punk::config toml), G-042 (subshell help topics), G-043 (subshell definition plugins).
- Console err channel: G-011.
- Launch surface: G-023 (version-named binaries), G-033 (`proj:` package mode), G-077 (`-e` one-liners), G-089 (scriptlib in kits).
- 8.6 buildsuite family: G-099 (core runtime, achieved 2026-07-26), G-100 (dependency set), G-101 (kit container).
- 8.6 buildsuite family: G-101 (kit container) - the core runtime (G-099) and the Tk/tklib/tcllib dependency set (G-100) are achieved 2026-07-26.
## Doc map - which lane holds what

4
GOALS-archive.md

@ -190,3 +190,7 @@ Acceptance: `make.tcl bakelist` under a plain tclsh and under a punk kit executa
### G-099 [achieved 2026-07-26] suite_tcl86: zig buildsuite for core-8-6-branch (runtime, thread, tclvfs, core-test gate) → detail: goals/archive/G-099-suite-tcl86-buildsuite.md
Scope: src/buildsuites/suite_tcl86/ (new: suite.tcl driver, 8.6 zig recipe + helper modules, expected_test_failures baseline); src/buildsuites/_build/ (untracked sources/outputs via existing globs); bin/tools/ (pinned zig as consumed); PUNK_FOSSIL_STORE / ~/.punkshell/fossils (hermetic clone store shared with suite_tcl90)
Acceptance: from the pinned zig and fetched core-8-6-branch sources (hermetic clone store, no MS toolchain) the suite produces a working tclsh86 executable and tcl86 dll with an installed lib tree the shell resolves without external Tcl installations (TCLLIBPATH/TCL_LIBRARY unset in all suite child invocations); an 8.6-compatible thread package and tclvfs build and load in that shell (versions recorded); suite.tcl test runs the 8.6 core testsuite, parses totals (exit codes not trusted), and gates against a tracked expected_test_failures baseline with every entry dispositioned with a reason - a full-run PASS deterministic across two consecutive runs is recorded in this file; an interactive-smoke record (script execution, package require checks) is captured here.
### G-100 [achieved 2026-07-26] suite_tcl86 dependency set: Tk 8.6 + tklib, tcllib with critcl accelerators, punkshell runtests on the 8.6 runtime → detail: goals/archive/G-100-suite-tcl86-tk-tcllib.md
Scope: src/buildsuites/suite_tcl86/ (recipe extensions, driver steps); src/buildsuites/_build/ (tk/tklib/tcllib/critcl sources and outputs); bin/tools/ (pinned zig as consumed); src/tests/ (punkshell suites exercised against the suite-built 8.6 runtime)
Acceptance: from the pinned zig and fetched sources the suite produces a Tk 8.6 the suite tclsh loads (package require Tk with a create/destroy-window verification recorded here) and a matching tklib whose packages resolve (a representative require recorded); a tcllib install via its installer whose accelerators load (package require tcllibc) with the accelerated-vs-pure comparison recorded; punkshell runtests runs against the suite 8.6 runtime and its census is recorded here against a same-day native/kit 8.6 baseline run (same include set), with every difference dispositioned - Tk-dependent suites run unskipped, and any 8.6-runtime-only failures are root-caused or baselined with reasons.

4
GOALS.md

@ -349,10 +349,6 @@ Detail: goals/G-089-scriptlib-kits-and-modes.md
Scope: src/modules/shellfilter-999999.0a1.0.tm (stack placement, insert_transform, junctions); src/modules/shellthread-999999.0a1.0.tm (worker park/adoption/termination); src/tests/modules/ (new characterization suites); README.md ('very unripe parts' note)
Detail: goals/G-090-shellfilter-shellthread-audit.md
### G-100 [proposed] suite_tcl86 dependency set: Tk 8.6 + tklib, tcllib with critcl accelerators, punkshell runtests on the 8.6 runtime
Scope: src/buildsuites/suite_tcl86/ (recipe extensions, driver steps); src/buildsuites/_build/ (tk/tklib/tcllib/critcl sources and outputs); bin/tools/ (pinned zig as consumed); src/tests/ (punkshell suites exercised against the suite-built 8.6 runtime)
Detail: goals/G-100-suite-tcl86-tk-tcllib.md
### G-101 [proposed] kit container strategy for zig-built Tcl 8.6 runtimes (no zipfs)
Scope: investigation + decision record under src/buildsuites/suite_tcl86/ (or successor mechanism); TEMP_REFERENCE/metakit + TEMP_REFERENCE/KitCreator (read-only guidance); src/runtime/mapvfs.config ('kit' type consumers); src/make.tcl kit-wrap path (as consumer)
Detail: goals/G-101-tcl86-kit-container-strategy.md

2
goals/G-005-zig-build-infrastructure.md

@ -51,7 +51,7 @@ This goal is the primary mechanism for G-004's outcome. It is independent of and
respect - see goals/archive/G-058-static-runtime-packages.md.
- Buildsuite-arc relationship (recorded 2026-07-24 after overlap review; neither this
file nor the buildsuite goals previously referenced each other): the zig buildsuite
system - achieved G-096/G-098/G-099/G-102/G-103/G-104/G-107, live G-100/G-105/G-108/
system - achieved G-096/G-098/G-099/G-100/G-102/G-103/G-104/G-107, live G-105/G-108/
G-116 - is the arm's-length runtime factory this goal's direction became, sharing the
pinned-zig / zig-only toolchain policy. This goal's distinct remainder: building the
VENDORED native extensions (twapi, tcllibc class) and wiring missing-binary retrieval

57
goals/G-100-suite-tcl86-tk-tcllib.md

@ -1,57 +0,0 @@
# G-100 suite_tcl86 dependency set: Tk 8.6 + tklib, tcllib with critcl accelerators, punkshell runtests on the 8.6 runtime
Status: proposed
Scope: src/buildsuites/suite_tcl86/ (recipe extensions, driver steps); src/buildsuites/_build/ (tk/tklib/tcllib/critcl sources and outputs); bin/tools/ (pinned zig as consumed); src/tests/ (punkshell suites exercised against the suite-built 8.6 runtime)
Goal: suite_tcl86 builds/installs the remaining runtime pieces punkshell's Tcl 8 testing depends on - Tk from fossil core-8-6-branch loadable by the suite's 8.6 windows runtime together with a matching tklib (installer.tcl where it does more than a straight copy), and tcllib installed via its documented installer machinery WITH its critcl-built C accelerators (tcllibc; critcl driving zig cc, bootstrapped by the suite-built tclsh86 - no MS toolchain) - and punkshell's own runtests runs against the suite-built 8.6 runtime with Tk-dependent suites unskipped.
Acceptance: from the pinned zig and fetched sources the suite produces a Tk 8.6 the suite tclsh loads (package require Tk with a create/destroy-window verification recorded here) and a matching tklib whose packages resolve (a representative require recorded); a tcllib install via its installer whose accelerators load (package require tcllibc) with the accelerated-vs-pure comparison recorded; punkshell runtests runs against the suite 8.6 runtime and its census is recorded here against a same-day native/kit 8.6 baseline run (same include set), with every difference dispositioned - Tk-dependent suites run unskipped, and any 8.6-runtime-only failures are root-caused or baselined with reasons.
## Context
Mirror of G-098 (suite_tcl90 dependency set, achieved 2026-07-20) for the 8.6 arc.
Depends on G-099 (suite_tcl86 core buildsuite) - ACHIEVED 2026-07-26 (see
goals/archive/G-099-suite-tcl86-buildsuite.md); the 8.6 runtime (tclsh86ts/
tclsh86t + tcl86t.dll + installed lib tree), thread 2.8.13, tclvfs 1.4.2 and
the core/thread/tclvfs test-gate this goal extends now exist, so the dependency
is met and G-100 is unblocked.
8.6-specific expectations (verify at activation):
- Tk core-8-6-branch pairs with the 8.6 runtime; the 9.0 build_tk zig module was
derived from tk9 makefile.vc object lists - the 8.6 lists/xlib arrangement differ,
so expect a re-derivation rather than reuse.
- tcllib trunk supports 8.5+; the same vendored critcl mechanism applies, with
critcl driven by the suite tclsh86. tcllibc built per-generation (the punk9wintk903
vfs precedent carries tcl8/tcl9 tcllibc side by side under lib_tcl8/lib_tcl9).
- runtests under 8.6: no tcl::process, so -jobs child spawning falls back to
blocking execs which CONVOY on windows (documented) - run -singleproc 1 or accept
serialized child walls for the census run; record which mode produced the census.
- Census baseline: compare against the punk86-class kit runtime results on the same
day/include-set (forward-slash path conventions on 8.6 kits noted in project
memory) rather than against the 9.x census.
- The kit-child 'src' package_mode launch added to runtests for punk kits (G-098
increment 7) applies to 8.6 punk kits too - a plain suite tclsh86 host takes the
native path.
SEQUENCING: as for G-099 - proposed now, activation recommended after suite_tcl90's
G-096 (achieved 2026-07-20) remainder closed and punk905_beta trial concludes.
## Notes
- G-116 relationship (recorded 2026-07-24 after overlap review): G-116's zig-built
crypto-backend + static tcltls pattern (suite_tcl90) is the reference if the 8.6
dependency set later grows a tls battery - the bi-family payload contract it
extends is generation-shaped, not 9-only.
- G-005 relationship: same zig toolchain-policy lineage as G-099's note - G-005
owns the make.tcl-integrated vendored-extension build direction; the suites are
its arm's-length proving ground.
- G-099 (achieved, see goals/archive/G-099-suite-tcl86-buildsuite.md) delivered
the patterns this goal extends: the build86.zig recipe + build_tclthread86/
build_tclvfs86 helpers (TEA-shape companion builds against the 8.6 stub lib),
the whole-tree library install + tm-module derivation, and the suite_tcl90
G-107 (achieved - goals/archive/G-107-buildsuite-library-tests.md) policy-tier
test-gate (test_gate.tcl) - a Tk/tklib/tcllib gate follows the same shape. Two 8.6-specific traps recorded there carry forward: the
load-bearing MP_FIXED_CUTOFFS define and TCL_BROKEN_MAINARGS+console-subsystem
entry handling (build86.zig comments + README DERIVATION). Also inherited from
G-099 Notes: the G-106 powershell console-mode disableraw-via-server path is
only tcl9-verified - now that a twapi-less 8.6 runtime exists, run the G-106
recipe against it (goals/archive/G-106-powershell-consolemode-fallback.md).

18
goals/G-101-tcl86-kit-container-strategy.md

@ -37,12 +37,24 @@ well-understood ports of the 9.0 work, while this is open investigation - keepin
it out of their acceptance keeps the 8.6 suite arc incrementally landable.
SEQUENCING: proposed; naturally last in the 8.6 arc (needs the G-099 runtime to
wrap). G-099 landed 2026-07-26 (achieved - see
goals/archive/G-099-suite-tcl86-buildsuite.md), so the runtime this goal wraps
now exists; activation still waits on G-100 (the dependency set) landing.
wrap). Both predecessors have landed - G-099 (achieved 2026-07-26 - see
goals/archive/G-099-suite-tcl86-buildsuite.md) built the runtime this goal wraps,
and G-100 (achieved 2026-07-26 - see goals/archive/G-100-suite-tcl86-tk-tcllib.md)
completed the dependency set that becomes this container's payload. The arc is
unblocked; activation is the user's call.
## Notes
- Related: G-116 (suite-built tcltls with a zig-built crypto backend) - the
payload question is shared: whatever container this goal picks carries the
battery set, and G-116's bi-family payload contract is generation-shaped, not
9-only. Recorded 2026-07-26 at the G-100 archive sweep, where the pair's only
bridges (G-100, G-103) both left the live tier.
- Payload inventory as of 2026-07-26: G-100 (achieved - see
goals/archive/G-100-suite-tcl86-tk-tcllib.md) completed the 8.6 dependency set
this container would wrap - Tk 8.6.17 (tk86t.dll + lib/tk8.6), tklib 0.9,
tcllib 2.0 + tcllibc accelerators, alongside G-099's thread 2.8.13 / tclvfs
1.4.2 / registry / dde.
- 2026-07-21: the prebuilt runtimes this goal replaces (tclkit86bi.exe,
tclkit-win64-dyn.exe) are Metakit tclkits - self-contained (Tcl + VFS
embedded) yet DYNAMICALLY linked to libc. That dynamic libc is what lets them

5
goals/G-105-buildsuite-cross-target.md

@ -33,8 +33,9 @@ INVERTED):
musl-dynamic).
Relationships: G-060 (qemu cross-platform matrix - WSL is the cheap first rung;
qemu covers what WSL cannot), G-099/G-100 (the 8.6 suite inherits the target
dimension), G-102 (achieved 2026-07-21 - driver shape settled; target becomes a build-graph dimension), G-103 (achieved 2026-07-22 - see goals/archive/G-103-runtime-kit-family.md)
qemu covers what WSL cannot), G-099/G-100 (both achieved 2026-07-26 - the 8.6
suite and its Tk/tklib/tcllib dependency set inherit the target dimension; see
goals/archive/G-100-suite-tcl86-tk-tcllib.md), G-102 (achieved 2026-07-21 - driver shape settled; target becomes a build-graph dimension), G-103 (achieved 2026-07-22 - see goals/archive/G-103-runtime-kit-family.md)
(family x target = the punkbin artifact matrix), G-006 (punkbin artifact consent).
## Notes

6
goals/G-108-buildsuite-debug-tier.md

@ -55,6 +55,12 @@ TCL_COMPILE_STATS similarly must be absent (not =0) for release performance.
## Notes
- G-100 (archived) recorded a census-hygiene finding this goal's recorded test
runs depend on: `TCL<major>_<minor>_TM_PATH` env vars seed tm.tcl's module
roots and were NOT part of the suite scrub list, so a suite shell can silently
prefer machine/repo tm modules over what the suite installed. Fixed for
suite_tcl86; suite_tcl90 still has the exposure via `TCL9_0_TM_PATH` - see
goals/archive/G-100-suite-tcl86-tk-tcllib.md.
- G-104 (archived) recorded this goal as a surface consumer: 'make.tcl
buildsuite build' forwards driver args (-steps/-zigargs) untouched and
list/info read the sources.config self-description records, so the -dbg<n>

7
goals/G-116-suite-built-tcltls.md

@ -44,6 +44,13 @@ extends).
## Notes
- G-100 (achieved 2026-07-26 - goals/archive/G-100-suite-tcl86-tk-tcllib.md)
recorded two things this goal will hit when it adds its own sources: zig
0.16's `zig fetch` resolves a BUILD ROOT before fetching (run it from a
directory with a build.zig, e.g. the suite dir, or it dies with a misleading
"no build.zig file found"), and the suite env scrub must include
`TCL<major>_<minor>_TM_PATH` or a battery run measures the machine's module
trees instead of the suite's (suite_tcl90 still has that exposure).
- G-117 relationship (G-117 achieved 2026-07-24 - see
goals/archive/G-117-self-describing-runtimes.md; recorded 2026-07-24 after overlap review; neither file
previously referenced the other): this goal's Acceptance extends the artifact

210
goals/archive/G-100-suite-tcl86-tk-tcllib.md

@ -0,0 +1,210 @@
# G-100 suite_tcl86 dependency set: Tk 8.6 + tklib, tcllib with critcl accelerators, punkshell runtests on the 8.6 runtime
Status: achieved 2026-07-26
Scope: src/buildsuites/suite_tcl86/ (recipe extensions, driver steps); src/buildsuites/_build/ (tk/tklib/tcllib/critcl sources and outputs); bin/tools/ (pinned zig as consumed); src/tests/ (punkshell suites exercised against the suite-built 8.6 runtime)
Goal: suite_tcl86 builds/installs the remaining runtime pieces punkshell's Tcl 8 testing depends on - Tk from fossil core-8-6-branch loadable by the suite's 8.6 windows runtime together with a matching tklib (installer.tcl where it does more than a straight copy), and tcllib installed via its documented installer machinery WITH its critcl-built C accelerators (tcllibc; critcl driving zig cc, bootstrapped by the suite-built tclsh86 - no MS toolchain) - and punkshell's own runtests runs against the suite-built 8.6 runtime with Tk-dependent suites unskipped.
Acceptance: from the pinned zig and fetched sources the suite produces a Tk 8.6 the suite tclsh loads (package require Tk with a create/destroy-window verification recorded here) and a matching tklib whose packages resolve (a representative require recorded); a tcllib install via its installer whose accelerators load (package require tcllibc) with the accelerated-vs-pure comparison recorded; punkshell runtests runs against the suite 8.6 runtime and its census is recorded here against a same-day native/kit 8.6 baseline run (same include set), with every difference dispositioned - Tk-dependent suites run unskipped, and any 8.6-runtime-only failures are root-caused or baselined with reasons.
## Context
Mirror of G-098 (suite_tcl90 dependency set, achieved 2026-07-20) for the 8.6 arc.
Depends on G-099 (suite_tcl86 core buildsuite) - ACHIEVED 2026-07-26 (see
goals/archive/G-099-suite-tcl86-buildsuite.md); the 8.6 runtime (tclsh86ts/
tclsh86t + tcl86t.dll + installed lib tree), thread 2.8.13, tclvfs 1.4.2 and
the core/thread/tclvfs test-gate this goal extends now exist, so the dependency
is met and G-100 is unblocked.
8.6-specific expectations (verify at activation):
- Tk core-8-6-branch pairs with the 8.6 runtime; the 9.0 build_tk zig module was
derived from tk9 makefile.vc object lists - the 8.6 lists/xlib arrangement differ,
so expect a re-derivation rather than reuse.
- tcllib trunk supports 8.5+; the same vendored critcl mechanism applies, with
critcl driven by the suite tclsh86. tcllibc built per-generation (the punk9wintk903
vfs precedent carries tcl8/tcl9 tcllibc side by side under lib_tcl8/lib_tcl9).
- runtests under 8.6: no tcl::process, so -jobs child spawning falls back to
blocking execs which CONVOY on windows (documented) - run -singleproc 1 or accept
serialized child walls for the census run; record which mode produced the census.
- Census baseline: compare against the punk86-class kit runtime results on the same
day/include-set (forward-slash path conventions on 8.6 kits noted in project
memory) rather than against the 9.x census.
- The kit-child 'src' package_mode launch added to runtests for punk kits (G-098
increment 7) applies to 8.6 punk kits too - a plain suite tclsh86 host takes the
native path.
SEQUENCING: as for G-099 - proposed now, activation recommended after suite_tcl90's
G-096 (achieved 2026-07-20) remainder closed and punk905_beta trial concludes.
## Progress
### 2026-07-26 activation + increment 1 (dependency set built, census taken)
- ACTIVATED by the user. Sources wired into both flows: `sources.config` gains
tk (`core-8-6-branch` -> dir `tk86`), tklib/tcllib (trunk) and critcl (git
3.3.1); `build.zig.zon` gains the matching pins - tk freshly `zig fetch`ed
(checkin 9986fdc267, 2025-03-17 = Tk 8.6.17), tklib/tcllib/critcl re-fetched
and verified BYTE-IDENTICAL to suite_tcl90's pins (generation-neutral
upstreams; what differs per suite is the shell running the installers and the
critcl target). suite.tcl gains git-source support + the new staging items;
the bootstrap driver's `source_dirs`/`recipe_items` match.
- TK 8.6 RE-DERIVED (`build_tk86/build_tk86.zig`) from the staged 8.6
`win/makefile.vc` + `rules.vc`, not ported from the 9.0 module - the goal
Context's expectation held. Full derivation record in
`src/buildsuites/suite_tcl86/README.md`; the load-bearing differences:
no `tkPkgConfig.c` (so NO `CFG_*` defines at all), the
TCL_MAJOR_VERSION==8 naming arm (`tk86t.dll`, `lib/tk8.6`), the
`@TK_WIN_VERSION@` = `$(DOTVERSION).0.0` manifest substitution (not the
tcl-style releaselevel formula), and an 8.6 object inventory without
tkIcu/tkImgListFormat/tkImgSVGnano/tkWinGDI/tkWinIco/tkWinSysTray. Products:
`bin/tk86t.dll` + `lib/tk86t.lib` + the `lib/tk8.6` script library +
makefile-shaped `pkgIndex.tcl`.
- TK ACCEPTANCE EVIDENCE (repeatable - `tools/tk_smoke.tcl` runs in the `smoke`
step on every build, and a load-only check would pass with no usable display):
`tk_smoke OK: Tk 8.6.17 (Ttk 8.6.17) windowingsystem win32 screen 3840x2160
depth 32 widgets Label/TButton/Toplevel create+map+destroy clean`. Both shells
load it: `pkg_smoke OK: Thread:2.8.13 vfs:1.4.2 registry:1.3.5 dde:1.4.6
Tk:8.6.17` (static AND dynamic).
- TKLIB via its OWN installer (batch, packages-only, explicit `-pkg-path` so the
installer's tcl_pkgPath-derived default can never place files off-tree) ->
`lib/tklib0.9`. Representative require recorded: `pkg_smoke OK: tooltip:2.0.4`
- a Tk-DEPENDENT package resolving through the installer-generated aggregate
pkgIndex.
- TCLLIB 2.0 installed the same way (`md5:2.0.9 sha1:2.0.5 cmdline:1.5.3`), and
TCLLIBC built by critcl 3.3.1 driving `zig cc` under the suite-built 8.6 shell
(tracked `critcl_zig.config`, target pinned `win32-x86_64-zig` - critcl
otherwise probes PATH and falls back to its builtin msvc target). Accelerator
evidence, both on every build: `pkg_smoke OK: tcllibc:2.0 md5-critcl-accel:1`
and the new `tools/accel_bench.tcl` -
`accel_bench OK: sha1 over 4198400 bytes - critcl 5.9ms vs pure-tcl 2661.4ms
= 452.3x (digests identical, sha1 2.0.5, tcllibc 2.0)`. The bench loads BOTH
implementations via `::sha1::LoadAccelerator` and switches with
`::sha1::SwitchTo`, so the comparison is in-process and the digests are
compared rather than assumed.
- TEST TIERS extended on the G-107 policy pattern: `test-tcllib` / `test-tklib`
(record tier, inside `test-libraries`) and the OPT-IN `test-tk` (excluded from
`test-libraries`; `winDialog.test`/`winMsgbox.test` excluded by default -
their message-injection automation blocks on a human under a batch shell).
- HERMETIC-ENVIRONMENT FINDING (see Notes): `TCL<major>_<minor>_TM_PATH` was
never scrubbed, so the suite shells were silently preferring repo-built tm
modules over the tcllib the suite had just installed. Fixed in all three
scrub sites before the census was taken.
#### punkshell runtests census (2026-07-26, both runs same day, same include set)
Mode: DEFAULT single-process sequential (`-singleproc 1`; no `-jobs` - 8.6 has
no `tcl::process`, so parallel child spawning re-convoys on windows, per the
goal Context). Identical invocation for both runtimes, full source-tree suite,
no include filter: `<shell> src/tests/runtests.tcl -report json`.
- BASELINE - native Tcl 8.6.17 (`C:/Users/sleek/AppData/Local/Apps/Tcl86`,
which carries Tk 8.6.17, Thread 2.8.12, tcllibc 2.0, twapi): 101 files,
Total 1003 / Passed 986 / Skipped 10 / Failed 7, 657s. Files failed:
`core/tcl/.../exec.test` (ERROR `invalid command name "tcl::build-info"` - a
9-era core test under an 8.6 shell), `modules/punk/ns/.../nslist.test` (4),
`modules/punk/ns/.../nsprimitives.test` (2 - the `:::odd` namespace-lookup
class), `modules/punkcheck/.../concurrency.test` (1).
- SUITE RUNTIME - `out/bin/tclsh86ts.exe` (8.6.18, this suite's static shell):
101 files, Total 1003 / Passed 987 / Skipped 10 / Failed 6, 601s.
- DIFFERENCE SET (authoritative, `scriptlib/developer/runtests_parity.tcl`):
exactly ONE - `punkcheck concurrency_two_writers_no_corruption` ERRORed in the
BASELINE run and PASSED on the suite runtime. Every other per-file status,
tally, failure identity, skip identity+reason and warning code is identical.
DISPOSITION: flake under full-suite load, not a runtime difference - the file
passes 6/6 in isolation on BOTH runtimes, twice each (verified same day).
ZERO suite-runtime-only failures; nothing needed baselining.
- TK-DEPENDENT SUITES RAN UNSKIPPED on the suite runtime:
`modules/punk/repl/testsuites/repl/consolebackends.test` 3/3 PASS (its
tkconsole backend drives a child repl on the suite-built Tk) and
`modules/opunk/console/testsuites/console/backends.test` 7 passed / 1 skipped.
That single skip is `backends_tkconsole_widget_size_and_eof`, whose
`tkavailable` constraint requires the `PUNK_TEST_TK` OPT-IN env var (not Tk
availability); it skips identically on the native baseline, and the opt-in's
own pre-existing quirk is recorded in G-098's increment 6 notes.
- Runtime-difference note (does not change any result): the suite runtime has no
twapi, so its runs open with `is_interactive cannot detect console ... without
twapi` and a `punk::du` slower-disk-operations warning that the native
baseline (twapi present) does not print.
#### library test tiers - first censuses (2026-07-26, suite tclsh86ts.exe 8.6.18)
Beyond the acceptance text; run because the goal Context asks the G-107
policy-tier shape to follow for the new libraries.
- thread (gate): 142 run / 126 passed / 16 skipped / 0 failed - GATE PASS
(baseline still empty; constraint-driven skips only).
- tklib (RECORD, new tier): 796 run / 684 passed / 112 skipped / 0 failed. Its
Tk-dependent suites exercise the suite-built Tk, and nothing failed.
- tcllib (RECORD, new tier): 64259 run / 61186 passed / 3032 skipped / 41
failed - names in `out/testreports/tcllib.summary`. Dominant classes:
network-reaching suites (picoirc 20, httpd-proxy/cgi 4), installer/path
assumptions (install-2.1/2.2, find-sf-*/find-4.0, multi/multiop-5.2), plus
matrix-16.7/16.8, tar-add, uri-4.1, example-*, zipfile-encode-6.4/6.5. A
record tier is evidence, not a gate criterion - no baseline is tracked and the
step never fails the build on these.
- tclvfs (gate): the tcllib install CHANGED this suite's census - vfsFtp-1.1..
1.3 sit behind an `ftp` constraint that needs tcllib's ftp package (absent at
the G-099 capture: 3 constraint skips), so they now RUN and reach the retired
`ftp://ftp.tcl.tk`, the same external-service class already baselined for
vfsUrl-1.2/2.1 (skips 8 -> 5 accordingly). `expected_test_failures_tclvfs.txt`
extended with the three names and the cause; re-run confirms GATE PASS
(51 run / 42 passed / 5 skipped / 4 failed, all baselined; vfsUrl-1.2 noted as
a stale entry - which members of the family fail is network-timing dependent,
so stale notes are expected and never fatal).
Remains for acceptance: nothing - the four acceptance clauses (Tk loadable +
create/destroy verification, matching tklib with a representative require,
tcllib installed by its own machinery with loading accelerators and a recorded
accelerated-vs-pure comparison, and a dispositioned runtests census against a
same-day 8.6 baseline with Tk suites unskipped) are all recorded above.
## Notes
- Hermetic-environment finding 2026-07-26 (fixed here, exposure recorded for
suite_tcl90): `scrubTclEnv` cleared only TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY,
but `tm.tcl` also seeds module roots from `env(TCL<major>_<minor>_TM_PATH)`
down through every lower minor. This machine points `TCL8_6_TM_PATH` at
`C:/repo/jn/shellspy/modules` among others, so suite shells preferred a
repo-built `sha1-2.0.4.tm` / `md5 2.0.8` over the tcllib 2.0.5 / 2.0.9 the
suite had just installed - a census through such a shell measures the machine.
Scrub list now covers `TCL8_0..8_7_TM_PATH` + `TCL9_0/9_1_TM_PATH` in the
per-step scrub (`build_common.zig` `tcl_env_vars`), `suite.tcl`'s
process-level unset and the bootstrap nested-build scrub. suite_tcl90 has the
same exposure via `TCL9_0_TM_PATH` and was deliberately left untouched (out of
this goal's scope) - worth a suite_tcl90-scoped fix.
- zig 0.16 trap (recorded while pinning): `zig fetch <url>` resolves a BUILD
ROOT before fetching, so it must be run from a directory containing a
`build.zig`; from anywhere else it dies with the misleading "no build.zig file
found, in the current directory or any parent directories". Also in the
`build.zig.zon` header.
- Activation freshness 2026-07-26: goals_xref.tcl score G-100 re-run at the
proposed->active flip (user-directed activation). Top pairs G-105 / G-101 /
G-116 / G-108 reviewed; the two that were only one-directional get the
back-pointers below, G-116 was already recorded here, and the remaining hits
are term noise - no new relationships.
- Related: G-105 (buildsuite cross-target) - it names this arc as a target-
dimension consumer; the tk/tklib/tcllib/critcl steps this goal adds must not
preclude a per-target dimension (no per-target tree copies, per-target out
prefixes).
- Related: G-101 (8.6 kit container strategy) - the downstream consumer of this
dependency set: whatever container it picks carries the Tk/tklib/tcllib/
tcllibc payload this goal produces.
- Related: G-108 (debug buildsuite tier) - sibling suite-arc goal queued behind
this arc; its -dbg<n>/tcl::test variant pattern is suite_tcl90-scoped today.
- G-116 relationship (recorded 2026-07-24 after overlap review): G-116's zig-built
crypto-backend + static tcltls pattern (suite_tcl90) is the reference if the 8.6
dependency set later grows a tls battery - the bi-family payload contract it
extends is generation-shaped, not 9-only.
- G-005 relationship: same zig toolchain-policy lineage as G-099's note - G-005
owns the make.tcl-integrated vendored-extension build direction; the suites are
its arm's-length proving ground.
- G-099 (achieved, see goals/archive/G-099-suite-tcl86-buildsuite.md) delivered
the patterns this goal extends: the build86.zig recipe + build_tclthread86/
build_tclvfs86 helpers (TEA-shape companion builds against the 8.6 stub lib),
the whole-tree library install + tm-module derivation, and the suite_tcl90
G-107 (achieved - goals/archive/G-107-buildsuite-library-tests.md) policy-tier
test-gate (test_gate.tcl) - a Tk/tklib/tcllib gate follows the same shape. Two 8.6-specific traps recorded there carry forward: the
load-bearing MP_FIXED_CUTOFFS define and TCL_BROKEN_MAINARGS+console-subsystem
entry handling (build86.zig comments + README DERIVATION). Also inherited from
G-099 Notes: the G-106 powershell console-mode disableraw-via-server path is
only tcl9-verified - now that a twapi-less 8.6 runtime exists, run the G-106
recipe against it (goals/archive/G-106-powershell-consolemode-fallback.md).

13
src/buildsuites/AGENTS.md

@ -40,8 +40,11 @@ this project's `src/` tree.
- `suite_tcl90/` - Tcl 9.0 suite (its README.md carries lineage/detail; no
child AGENTS.md yet)
- `suite_tcl86/` - Tcl 8.6 suite, G-099 arc (recipe landed 2026-07-26:
tclsh86ts/tclsh86t + tcl86t.dll + lib tree, thread 2.8.13 + tclvfs 1.4.2,
smokes green; test censuses/baselines in progress - its README.md carries
the deliverables, source selections and the makefile.vc DERIVATION record;
no child AGENTS.md yet)
- `suite_tcl86/` - Tcl 8.6 suite (G-099 achieved 2026-07-26: tclsh86ts/tclsh86t
+ tcl86t.dll + lib tree, thread 2.8.13 + tclvfs 1.4.2, core/thread/tclvfs
gates; G-100 active: Tk 8.6.17 + tklib 0.9 + tcllib 2.0/tcllibc, and
punkshell's own runtests censused on this runtime). Its README.md carries the
deliverables, source selections, both makefile.vc DERIVATION records and the
hermetic-environment finding (TCL<major>_<minor>_TM_PATH must be scrubbed
alongside TCLLIBPATH/TCL_LIBRARY or a census measures the machine); no child
AGENTS.md yet

144
src/buildsuites/suite_tcl86/README.md

@ -1,18 +1,18 @@
# suite_tcl86 - zig buildsuite for Tcl core-8-6-branch (G-099)
# suite_tcl86 - zig buildsuite for Tcl core-8-6-branch (G-099, G-100)
Builds a Tcl 8.6 windows runtime from fossil core-8-6-branch sources with the
pinned zig toolchain only (zig-only build policy; msys/mingw and MS toolchains
remain test/validation-only), together with the core binary companions
punkshell's Tcl 8 targets need - thread 2.8 and tclvfs - gated against the 8.6
core testsuite. Forked from `suite_tcl90` (the shape reference); shared
scaffolding stays duplicated-with-a-note until this second consumer makes the
right factoring obvious (G-099 Context).
STATUS: recipe LANDED 2026-07-26 - full `suite.tcl build` PASS (build 8.6.18 +
install + install-libraries + smokes on both shells, hermetic resolution
proven). Core/thread/tclvfs test censuses + dispositioned baselines are the
remaining G-099 acceptance items. Progress ledger:
`goals/G-099-suite-tcl86-buildsuite.md`.
remain test/validation-only), together with the binary and script companions
punkshell's Tcl 8 targets need - thread 2.8, tclvfs, Tk 8.6, tklib, and tcllib
with its critcl-built accelerators - gated against the 8.6 core testsuite.
Forked from `suite_tcl90` (the shape reference); shared scaffolding stays
duplicated-with-a-note until this second consumer makes the right factoring
obvious (G-099 Context).
STATUS: G-099 ACHIEVED 2026-07-26 (runtime + thread/tclvfs + core-test gate -
see `goals/archive/G-099-suite-tcl86-buildsuite.md`). G-100 (the dependency set:
Tk + tklib + tcllib/tcllibc, and punkshell's own runtests on this runtime) is
ACTIVE - see `goals/G-100-suite-tcl86-tk-tcllib.md` for the progress ledger.
## Deliverables (per the G-099 acceptance; stock threaded-release naming)
@ -33,11 +33,38 @@ remaining G-099 acceptance items. Progress ledger:
library pkgIndex files (http 2.9.9, msgcat 1.6.1, tcltest 2.5.11,
platform 1.1.1, platform::shell 1.1.4).
- `suite.tcl test` runs the 8.6 core testsuite, parses totals (exit codes not
trusted), and gates against a tracked `expected_test_failures.txt` baseline -
the FIRST CENSUS is in flight (record mode; the 9.0.5 census does not
transfer; machine traits dispositioned there will likely reappear under 8.6
test names). Same census flow for the thread/tclvfs suites
(`expected_test_failures_thread.txt` / `_tclvfs.txt`).
trusted), and gates against the tracked dispositioned `expected_test_failures.txt`
baseline (2 entries: the windows admin-share self-globbing machine trait).
Same flow for the thread/tclvfs suites (`expected_test_failures_thread.txt`
empty / `_tclvfs.txt` 2 external-FTP entries).
## Deliverables (G-100 dependency set)
- Tk **8.6.17** from `core-8-6-branch`: `bin/tk86t.dll` (+ `lib/tk86t.lib`
import lib) and the script library at `lib/tk8.6` with the makefile-shaped
`pkgIndex.tcl` - `package require Tk` loads in BOTH shells, and the `smoke`
step's `tools/tk_smoke.tcl` maps a real toplevel with classic + themed
widgets, updates, and destroys it (a load-only check would pass with no
usable display).
- tklib **0.9** installed by its OWN `installer.tcl` (batch, packages only,
explicit `-pkg-path`): not a straight copy - module exclusions, app-script
generation and the aggregating top-level `pkgIndex.tcl` that makes the
two-levels-down module indexes resolvable. Smoke: `package require tooltip`
(a Tk-DEPENDENT tklib package) through that aggregate index.
- tcllib **2.0** installed by its own installer (same family; its critcl DSL
procs are stubbed to no-ops there - that is the pure-tcl side), plus
**tcllibc** built by critcl 3.3.1 driving `zig cc` (`critcl_zig.config`,
target pinned `win32-x86_64-zig`) under the suite-built 8.6 shell. Evidence
on every build: `pkg_smoke -accel md5 tcllibc` (asserts
`::md5::accel(critcl)`) and `tools/accel_bench.tcl` - sha1 over 4MB,
**critcl 5.9ms vs pure-tcl 2661ms = ~450x**, digests identical.
- Library test tiers extended (G-107 policy pattern): `test-tcllib` /
`test-tklib` on the RECORD tier inside `test-libraries`, plus the OPT-IN
`test-tk` (excluded from `test-libraries`: it maps windows on the interactive
desktop for tens of minutes; the native-dialog suites `winDialog.test` /
`winMsgbox.test` are excluded by default because their message-injection
automation blocks waiting on a human under a batch shell - re-include with
`-Dtestnotfiles-tk=""`).
## Sources (both flows carry the same identity)
@ -59,6 +86,19 @@ Selections (2026-07-25, staged and verified):
(1.5.0) is tcl9-only (user decision 2026-07-22, recorded in suite_tcl90's
sources.config).
Selections (2026-07-26, G-100 dependency set):
- tk: `core-8-6-branch` -> dir `tk86` - tip checkin 9986fdc267 (2025-03-17) =
**8.6.17** (TK_PATCH_LEVEL in the staged tree). The 8.6 line is quiet
upstream; it pairs with the 8.6.18 core without complaint.
- tklib / tcllib / critcl: GENERATION-NEUTRAL - the same trunk refs and the
same content-hashed zon pins suite_tcl90 carries (re-fetched 2026-07-26 and
verified byte-identical). What differs per suite is the SHELL that runs the
installers and the critcl target that builds tcllibc; critcl compiles against
its own bundled headers, chosen by the version of the shell running it
(`lib/critcl/critcl_c/tcl8.6/` here), which is why one config file serves
both generations.
## DERIVATION (build86.zig from the staged 8.6.18 win/makefile.vc)
Recorded findings from the staged tree (assess-not-assume items from the G-099
@ -112,12 +152,78 @@ tclvfs test gates. Two extra findings surfaced during the build (recorded in
the DERIVATION list above): the load-bearing MP_FIXED_CUTOFFS, and the
vfs142t.dll TEA name that lets Tcl's load-time name guessing derive Vfs_Init.
## DERIVATION (build_tk86 from the staged tk 8.6 win/makefile.vc)
A RE-DERIVATION, not a port of suite_tcl90's `build_tk.zig` (the G-100 Context
predicted this). Verified against the staged tree 2026-07-26:
- Object inventory: 8.6 `TKOBJS` has no `tkIcu` / `tkImgListFormat` /
`tkImgSVGnano` / `tkPkgConfig`, and no `tkWinGDI` / `tkWinIco` /
`tkWinSysTray`. `XLIBOBJS` is the same 5-file X emulation set but is a
first-class makefile macro here (in the 9.0 derivation those files had to be
recovered by hand after a name-pattern extraction missed them). Every name in
the staged makefile resolved to exactly one source dir - the recipe's lists
are that mapping.
- NO `generic/tkPkgConfig.c` in 8.6 -> the recipe passes no `CFG_*` defines at
all (the 9.0 module carries a full `CFG_{RUNTIME,INSTALL}_*` set for it).
- Naming: `rules.vc` TCL_MAJOR_VERSION==8 arm gives `TKLIBNAME` = **tk86t.dll**
(SUFX `t` survives while TCL_VERSION <= 86); the 9.0 arm produces the
`tcl9tk90.dll` form. Script install dir is `lib/tk8.6`.
- `wish.exe.manifest`: 8.6's makefile substitutes `@TK_WIN_VERSION@` with
`$(DOTVERSION).0.0` = "8.6.0.0" - NOT the tcl-style
`<dotversion>.<releaselevel>.<patchlevel>` formula (`common.winResourceVersion`)
the 9.0 tree uses. `win/rc/tk.rc` embeds the manifest as RT_MANIFEST in the
DLL itself (that is what enables comctl32 v6 themed scrollbars), so it is
generated into the build cache and delivered through the rc include paths.
- `tkUuid.h`: 8.6 DOES generate it (`win/tkUuid.h.in` + `manifest.uuid`,
makefile.vc:541) and `tkWindow.c` includes it - same overlay treatment as the
tcl side.
- Stubs: `rules.vc` sets `USE_STUBS_DEFS` (`/DUSE_TCL_STUBS /DUSE_TCLOO_STUBS`)
for every project except tcl itself, so building Tk against the suite's
`libtclstub86.a` IS the stock arrangement - and it keeps the dll unbound to a
particular tcl dll name, so both the static and the dynamic suite shell load it.
- `tkMain.c` compiled twice (`tkMain.obj` + `tkMain2.obj` with UNICODE). The
ansi copy is forced with `-UUNICODE -U_UNICODE`; verified 2026-07-26 that the
pinned zig cc defines NEITHER for `x86_64-windows-gnu`, so the stock
ANSI/TCHAR arrangement of the 8.6 core is matched exactly (TCHAR resolution in
8.6 depends on `_UNICODE` at each TU's compile - tk and tcl must agree).
- Deviation from stock, recorded: the generated `pkgIndex.tcl` keeps only the
windows arm of the makefile's two-branch text (the unix DISPLAY branch cannot
apply to a windows-only suite) and wraps the load path in `file normalize`
(as the 9.0 recipe does).
## Hermetic environment (G-100 finding, 2026-07-26)
`scrubTclEnv` cleared only TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY. `tm.tcl` ALSO
seeds its module roots from `env(TCL<major>_<minor>_TM_PATH)` - for an 8.6
shell, `TCL8_6_TM_PATH` down through every lower minor - and this dev machine
points `TCL8_6_TM_PATH` at repo/foreign module trees. A suite shell therefore
preferred a repo-built `sha1-2.0.4.tm` over the tcllib 2.0.5 the suite had just
installed (and `md5 2.0.8` over 2.0.9): any census taken through such a shell
measures the machine, not the suite. The scrub list now covers
`TCL8_0..8_7_TM_PATH` and `TCL9_0/9_1_TM_PATH` in all three places - the
per-step scrub (`build_common.zig` `tcl_env_vars`), `suite.tcl`'s process-level
unset, and the bootstrap driver's nested-build scrub. Keep the two lists in
step. NOTE: suite_tcl90 has the same exposure via `TCL9_0_TM_PATH` and is
deliberately untouched here (out of this goal's scope).
## Flows
- FOSSIL DEV FLOW: `tclsh suite.tcl build|test|clean` (or via the make.tcl
surface: `tclsh src/make.tcl buildsuite build suite_tcl86 ?driver-args?`).
- NO-TCLSH FLOW: `zig build stage|bootstrap` - lands with the recipe;
`build.zig.zon` pins are already recorded.
Default build steps: `install install-libraries smoke tklib tcllib tcllibc`.
- NO-TCLSH FLOW: `zig build --build-file build86.zig stage|bootstrap` from THIS
tracked directory (materializes the `build.zig.zon` pins for any source that
has no checkout in the stage, then drives the staged recipe). `stage` verified
2026-07-26 - it skips the existing fossil/git checkouts and stages the recipe
items (including `build_tk86/` and `critcl_zig.config`).
- FINGERPRINT TRAP (fixed 2026-07-26): `build.zig.zon`'s `.fingerprint` low
half is a checksum OF THE PACKAGE NAME, so a hand-authored value is rejected
at configure time ("invalid fingerprint ... if this is a new or forked
package, use this value: ..."). The G-099-era value was hand-authored and
survived unnoticed because the staged `suite.tcl` flow never reads this file
- the first real `zig build` from the tracked dir surfaced it. If you fork
this suite, take the value zig prints rather than inventing one.
The suite self-description contract (records in `sources.config` read by
`make.tcl buildsuite list/info`) is documented in `src/buildsuites/README.md`.

48
src/buildsuites/suite_tcl86/build.zig.zon

@ -5,16 +5,23 @@
//tarball urls + zig package hashes; 'zig fetch <url>' prints a hash, branch-tip
//urls are deliberately NOT used - the pin IS the provenance record).
//
//The bootstrap build.zig driver lands with the recipe (build86.zig is a
//pending-derivation skeleton); these records pin the 8.6 sources from the
//suite's FIRST COMMIT so both flows carry the same source identity
//These records pin the 8.6 sources so both flows carry the same source identity
//(sources.config = live dev-flow refs on the same repositories/branches).
//Hashes below were computed with the pinned zig 0.16.0 ('zig fetch') on
//2026-07-25.
//Hashes computed with the pinned zig 0.16.0: the core set 2026-07-25, the
//G-100 dependency set 2026-07-26. TRAP: 0.16's 'zig fetch' resolves a build
//root BEFORE fetching, so it must be run from a directory with a build.zig
//(this suite dir does) - from anywhere else it dies with the misleading
//"no build.zig file found, in the current directory or any parent directories".
.{
.name = .suite_tcl86,
.version = "0.0.1",
.fingerprint = 0x9fe14fa771ae8617,
//zig-computed for the package name .suite_tcl86 - its low half is a checksum
//OF THAT NAME, so a hand-authored value is rejected at configure time
//('invalid fingerprint ... use this value: ...'). Corrected 2026-07-26 after
//the first real 'zig build --build-file build86.zig' from this tracked dir
//(the staged suite.tcl flow never reads this file, which is why the G-099-era
//hand-authored value survived unnoticed).
.fingerprint = 0x5c82bff30d9af9ad,
.minimum_zig_version = "0.16.0",
.dependencies = .{
//tcl core-8-6-branch checkout 1d1d5cbd91 2026-07-14 (8.6.17+ maintenance era)
@ -37,6 +44,35 @@
.hash = "N-V-__8AAIaeDQBJ9V3M96hpQSxemxghwLBG8fB1s0rqh4SQ",
.lazy = true,
},
//-- G-100 dependency set ------------------------------------------
//tk core-8-6-branch checkout 9986fdc267 2025-03-17 (8.6 maintenance line,
//pairing with the 8.6 runtime; the 9.0 suite pins core-9-0-branch instead)
.tk = .{
.url = "https://core.tcl-lang.org/tk/tarball/9986fdc26786cecc8d513e05373fce5bd6d7b2377d8e3b6d17fcb8f09b2d81fb/tk.tar.gz",
.hash = "N-V-__8AANjEKQE7NM2LsFNsAzQl7mtH8R82KTFHnPrCkBuH",
.lazy = true,
},
//tklib/tcllib/critcl are generation-neutral: the SAME pins suite_tcl90
//carries (verified identical by re-fetch 2026-07-26). What differs per
//suite is the shell that runs the installers and the critcl target.
//tklib trunk checkout 2d53beafc0 2026-07-03 (0.9 era)
.tklib = .{
.url = "https://core.tcl-lang.org/tklib/tarball/2d53beafc0e8621dacc75b3c809898cc9f1f92f9/tklib.tar.gz",
.hash = "N-V-__8AABomLAFd2U5OW96AfpQLoLm3Q9swpQZEwslrEJx3",
.lazy = true,
},
//tcllib trunk checkout c784f6fda4 2026-06-30 (2.0 era)
.tcllib = .{
.url = "https://core.tcl-lang.org/tcllib/tarball/c784f6fda4516f676453a2785b3c56f1ca5999f4/tcllib.tar.gz",
.hash = "N-V-__8AAFvlBARw4fIY107nOJmfxdwAjr1Vr89cYx98czpF",
.lazy = true,
},
//critcl 3.3.1 (tag commit 254bdffe2a)
.critcl = .{
.url = "https://github.com/andreas-kupries/critcl/archive/254bdffe2afc78148f79f92df8d6823a24f971bf.tar.gz",
.hash = "N-V-__8AAF8-bABDlxChmtnyalKgofVfN3kzcZdmZ7pjEmE0",
.lazy = true,
},
},
.paths = .{""},
}

232
src/buildsuites/suite_tcl86/build86.zig

@ -56,6 +56,7 @@ const common = @import("build_common.zig");
const build_zlib86 = @import("build_zlib86/build_zlib86.zig").build_zlib86;
const build_tclthread86 = @import("build_tclthread86/build_tclthread86.zig").build_tclthread86;
const tclvfs86 = @import("build_tclvfs86/build_tclvfs86.zig");
const tk86 = @import("build_tk86/build_tk86.zig");
const tcl_dot_version = "8.6";
const tcl_nodot_version = "86";
@ -616,6 +617,12 @@ pub fn build(b: *std.Build) !void {
build_tclvfs_step.dependOn(&tclvfs_compile.step);
try tclvfs86.install_tclvfs_package("../tclvfs", b, tclvfs_compile);
//-- Tk 8.6 (G-100): tk86t.dll + the tk script library + pkgIndex, all hooked
//onto the default install step inside the helper (as the companions above).
const tk_build = try tk86.build_tk86(tcl_source_folder, "../tk86", b, target, optimize, finalstublib);
const build_tk_step = b.step("build-tk", "build the Tk 8.6 loadable extension (tk86t.dll) + tk script library install");
build_tk_step.dependOn(&tk_build.lib.step);
// ================================================================
//install-libraries: the whole script library tree -> lib/tcl8.6 (each
//subdir package keeps its own pkgIndex; encoding/tzdata/msgs ride inside),
@ -682,18 +689,168 @@ pub fn build(b: *std.Build) !void {
//package-resolution smoke: the built companions + the library-dir
//packages load in each shell (Thread/vfs via lib/, registry/dde via
//the script library's package dirs - static registrations in the
//static shell, the built dlls in the dynamic one)
//static shell, the built dlls in the dynamic one; Tk via lib/tk8.6
//under a withdrawn root, so no window flashes here)
const pkgs = b.addSystemCommand(&.{sr.exe});
common.scrubTclEnv(pkgs);
pkgs.has_side_effects = true;
pkgs.setName(b.fmt("pkg_smoke {s}", .{sr.label}));
pkgs.addFileArg(b.path("tools/pkg_smoke.tcl"));
pkgs.addArgs(&.{ "Thread", "vfs", "registry", "dde" });
pkgs.addArgs(&.{ "-gui", "1", "Thread", "vfs", "registry", "dde", "Tk" });
pkgs.step.dependOn(b.getInstallStep());
pkgs.step.dependOn(install_libraries);
smoke_step.dependOn(&pkgs.step);
}
//-- Tk window-station smoke (G-100 acceptance evidence): maps a real
//toplevel with classic+themed widgets, updates, then destroys - a
//load-only check would pass without a usable display. Static shell only:
//the pkg_smoke runs above already prove Tk LOADS in both.
const tksmoke = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(tksmoke);
tksmoke.has_side_effects = true;
tksmoke.setName("tk_smoke static");
tksmoke.addFileArg(b.path("tools/tk_smoke.tcl"));
tksmoke.addArgs(&.{ "-expect", tk_build.patchlevel });
tksmoke.step.dependOn(b.getInstallStep());
tksmoke.step.dependOn(install_libraries);
smoke_step.dependOn(&tksmoke.step);
// ============ G-100 script-library dependency set ============
//Two shell roles here, deliberately split:
// INSTALLER/CRITCL runs use the build-cache artifact (addRunArtifact) so
// the step is CACHED on the shell's own hash and can own output dirs.
// That exe has no ../lib/tcl8.6 beside it, so those runs (and only
// those) get TCL_LIBRARY pointed at the staged source library.
// SMOKE runs use the INSTALLED shell with no TCL_LIBRARY, exactly as the
// smokes above: package resolution then comes from <prefix>/lib via the
// exe-relative auto_path entry - which is also what proves the install
// layout, not just the build.
const tcl_library_src = common.replaceAll(b, b.pathFromRoot(tcl_source_folder ++ "/library"), "\\", "/");
//-- tklib: pure-tcl, but its installer is NOT a straight copy (module
//exclusion list, app-script generation, and generation of the aggregating
//top-level pkgIndex.tcl that makes the two-levels-down module indexes
//resolvable). Batch mode, packages only; -pkg-path is a cached output dir
//installed into the prefix, so the installer's own default (derived from
//the RUNNING shell's tcl_pkgPath) can never place files off-tree.
const tklib_ver = common.installerPackageVersion(b, "../tklib");
const tklib_step = b.step("tklib", "install tklib via its own installer under the suite-built 8.6 shell (+Tk-dependent package smoke)");
const tklib_run = b.addRunArtifact(tclsh_static_exe);
common.scrubTclEnv(tklib_run);
tklib_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
//cached (non-side-effect) Runs spawn with stdout IGNORED - on windows the
//child then has no stdout handle and tcl errors on any puts ('can not find
//channel named "stdout"'). Capturing gives it a real pipe.
_ = tklib_run.captureStdOut(.{});
tklib_run.setCwd(b.path("../tklib"));
tklib_run.addFileArg(b.path("../tklib/installer.tcl"));
tklib_run.addArgs(&.{ "-no-gui", "-no-wait", "-no-apps", "-no-html", "-no-nroff", "-no-examples", "-pkg-path" });
const tklib_out = tklib_run.addOutputDirectoryArg("tklib-pkg");
const tklib_install = b.addInstallDirectory(.{
.source_dir = tklib_out,
.install_dir = .prefix,
.install_subdir = b.fmt("lib/tklib{s}", .{tklib_ver}),
});
const tklib_smoke = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(tklib_smoke);
tklib_smoke.has_side_effects = true;
tklib_smoke.setName("tklib pkg_smoke");
tklib_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tklib_smoke.addArgs(&.{ "-gui", "1", "tooltip" }); //a Tk-DEPENDENT tklib package
tklib_smoke.step.dependOn(&tklib_install.step);
tklib_smoke.step.dependOn(b.getInstallStep()); //Tk dll + pkgIndex + script library
tklib_smoke.step.dependOn(install_libraries);
tklib_step.dependOn(&tklib_smoke.step);
//-- tcllib: same installer family (its critcl DSL procs are stubbed to
//no-ops there - this is the PURE-TCL side; the accelerators are the
//tcllibc step below).
const tcllib_ver = common.installerPackageVersion(b, "../tcllib");
const tcllib_step = b.step("tcllib", "install tcllib (pure-tcl side) via its own installer under the suite-built 8.6 shell (+smoke)");
const tcllib_run = b.addRunArtifact(tclsh_static_exe);
common.scrubTclEnv(tcllib_run);
tcllib_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
_ = tcllib_run.captureStdOut(.{});
tcllib_run.setCwd(b.path("../tcllib"));
tcllib_run.addFileArg(b.path("../tcllib/installer.tcl"));
tcllib_run.addArgs(&.{ "-no-gui", "-no-wait", "-no-apps", "-no-html", "-no-nroff", "-no-examples", "-pkg-path" });
const tcllib_out = tcllib_run.addOutputDirectoryArg("tcllib-pkg");
const tcllib_install = b.addInstallDirectory(.{
.source_dir = tcllib_out,
.install_dir = .prefix,
.install_subdir = b.fmt("lib/tcllib{s}", .{tcllib_ver}),
});
const tcllib_smoke = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(tcllib_smoke);
tcllib_smoke.has_side_effects = true;
tcllib_smoke.setName("tcllib pkg_smoke");
tcllib_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tcllib_smoke.addArgs(&.{ "md5", "sha1", "cmdline" });
tcllib_smoke.step.dependOn(&tcllib_install.step);
tcllib_smoke.step.dependOn(b.getInstallStep());
tcllib_smoke.step.dependOn(install_libraries);
tcllib_step.dependOn(&tcllib_smoke.step);
//-- tcllibc: tcllib's critcl accelerators, critcl driving zig cc (the
//tracked critcl_zig.config; zig resolved from PATH with our own dir
//prepended). critcl compiles against ITS OWN bundled headers, chosen by
//the version of the shell running it (critcl_c/tcl8.6 here) - which is
//why the generation split works with one config file. sak.tcl's critcl
//target expects a tclkitsh on windows, so this replicates its underlying
//invocation directly, as suite_tcl90 does.
const tcllibc_step = b.step("tcllibc", "build+install tcllibc (critcl accelerators, zig cc) under the suite-built 8.6 shell (+accel smoke/bench)");
const critcl_run = b.addRunArtifact(tclsh_static_exe);
common.scrubTclEnv(critcl_run);
critcl_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
_ = critcl_run.captureStdOut(.{});
critcl_run.setCwd(b.path("../tcllib"));
critcl_run.addFileArg(b.path("../critcl/main.tcl"));
critcl_run.addArg("-config");
critcl_run.addFileArg(b.path("critcl_zig.config"));
//pin the build target to the config's explicit section: critcl otherwise
//GUESSES its target by probing PATH for compilers, and a scrubbed
//environment falls back to critcl's builtin msvc 'cl' target (G-102
//finding, inherited here).
critcl_run.addArgs(&.{ "-target", "win32-x86_64-zig" });
critcl_run.addArg("-cache");
_ = critcl_run.addOutputDirectoryArg("critcl-cache");
critcl_run.addArg("-force");
critcl_run.addArg("-libdir");
const critcl_libout = critcl_run.addOutputDirectoryArg("critcl-lib");
critcl_run.addArgs(&.{ "-pkg", "tcllibc" });
for (common.tcllibCritclFiles(b, "../tcllib")) |cf| {
critcl_run.addFileArg(b.path(b.pathJoin(&.{ "../tcllib/modules", cf })));
}
const zigdir = std.fs.path.dirname(b.graph.zig_exe) orelse ".";
const inherited_path = b.graph.environ_map.get("PATH") orelse "";
critcl_run.setEnvironmentVariable("PATH", b.fmt("{s}{c}{s}", .{ zigdir, std.fs.path.delimiter, inherited_path }));
const tcllibc_install = b.addInstallDirectory(.{
.source_dir = critcl_libout,
.install_dir = .prefix,
.install_subdir = "lib",
});
const tcllibc_smoke = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(tcllibc_smoke);
tcllibc_smoke.has_side_effects = true;
tcllibc_smoke.setName("tcllibc accel smoke");
tcllibc_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tcllibc_smoke.addArgs(&.{ "-accel", "md5", "tcllibc" });
tcllibc_smoke.step.dependOn(&tcllibc_install.step);
tcllibc_smoke.step.dependOn(&tcllib_install.step); //accel host packages come from tcllib
tcllibc_smoke.step.dependOn(b.getInstallStep());
tcllibc_smoke.step.dependOn(install_libraries);
//accelerated-vs-pure evidence (G-100 acceptance): same payload through
//sha1's critcl and pure-Tcl implementations, digests compared, ratio
//printed - the punkcheck-relevant hashing comparison.
const accel_bench = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(accel_bench);
accel_bench.has_side_effects = true;
accel_bench.setName("tcllibc accel bench");
accel_bench.addFileArg(b.path("tools/accel_bench.tcl"));
accel_bench.step.dependOn(&tcllibc_smoke.step);
tcllibc_step.dependOn(&accel_bench.step);
//-- test-gate: the 8.6 core testsuite under the installed static shell,
//judged on PARSED totals (all.tcl exit codes lie) vs the tracked
//dispositioned baseline. First census: run with -Dtestmode=record (the
@ -731,32 +888,57 @@ pub fn build(b: *std.Build) !void {
gate_run.step.dependOn(install_libraries);
gate_step.dependOn(&gate_run.step);
//-- per-library testsuites (suite_tcl90 G-107 pattern, thread+tclvfs
//arm): policy tiers gate|record|skip resolved at invocation.
//-- per-library testsuites (suite_tcl90 G-107 pattern): policy tiers
//gate|record|skip resolved at invocation.
// -Dtestpolicy-<lib>=gate|record|skip -Dtestargs-<lib>="..."
// -Dtestnotfiles-<lib>="a.test b.test"
//Baselines expected_test_failures_thread.txt / _tclvfs.txt are created
//from dispositioned record-mode censuses (same first-census flow as the
//core gate).
//core gate); the G-100 script-library suites ride the RECORD tier (their
//censuses are evidence, not a gate criterion - the same disposition
//suite_tcl90 made).
//NOTE zig runs independent steps concurrently - drive combined test
//invocations with -j1 (suite.tcl's test action does) so timing-sensitive
//suites never run beside heavy ones.
const LibSuite = struct {
name: []const u8,
testsdir: []const u8,
driver: []const u8,
name: []const u8, //step suffix, policy/testargs option suffix, report basename
testsdir: []const u8, //run cwd (from build root)
driver: []const u8, //driver script, relative to testsdir
default_policy: []const u8,
baseline: []const u8,
default_notfiles: []const u8,
baseline: []const u8, //tracked baseline filename ("" = none tracked yet)
default_notfiles: []const u8, //default driver -notfile exclusions ("" = none)
deps: []const *std.Build.Step, //beyond the top-level install step
};
const thread_test_deps = [_]*std.Build.Step{install_libraries};
const tclvfs_test_deps = [_]*std.Build.Step{install_libraries};
const tcllib_test_deps = [_]*std.Build.Step{ install_libraries, &tcllib_install.step, &tcllibc_install.step };
const tklib_test_deps = [_]*std.Build.Step{ install_libraries, &tklib_install.step, &tcllib_install.step };
const tk_test_deps = [_]*std.Build.Step{install_libraries};
const lib_suites = [_]LibSuite{
.{ .name = "thread", .testsdir = "../tclthread/tests", .driver = "all.tcl", .default_policy = "gate", .baseline = "expected_test_failures_thread.txt", .default_notfiles = "" },
.{ .name = "tclvfs", .testsdir = "../tclvfs/tests", .driver = "all.tcl", .default_policy = "gate", .baseline = "expected_test_failures_tclvfs.txt", .default_notfiles = "" },
.{ .name = "thread", .testsdir = "../tclthread/tests", .driver = "all.tcl", .default_policy = "gate", .baseline = "expected_test_failures_thread.txt", .default_notfiles = "", .deps = &thread_test_deps },
.{ .name = "tclvfs", .testsdir = "../tclvfs/tests", .driver = "all.tcl", .default_policy = "gate", .baseline = "expected_test_failures_tclvfs.txt", .default_notfiles = "", .deps = &tclvfs_test_deps },
//tcllib via its own support/devel/all.tcl (sak's underlying driver).
//The installed tcllibc is on the shell's package path, so
//accelerator-aware suites engage the critcl accelerators - the
//highest-value run of the set for an 8.6 runtime nobody else builds.
.{ .name = "tcllib", .testsdir = "../tcllib", .driver = "support/devel/all.tcl", .default_policy = "record", .baseline = "", .default_notfiles = "", .deps = &tcllib_test_deps },
.{ .name = "tklib", .testsdir = "../tklib", .driver = "support/devel/all.tcl", .default_policy = "record", .baseline = "", .default_notfiles = "", .deps = &tklib_test_deps },
//tk: OPT-IN (not part of test-libraries) - it maps windows on the
//interactive desktop for tens of minutes. The native-dialog suites are
//excluded by default: their message-injection automation does not
//drive the real OS dialogs under a batch shell, so each case blocks
//waiting on a human. Re-include with -Dtestnotfiles-tk="" for a
//manual interactive run.
.{ .name = "tk", .testsdir = "../tk86/tests", .driver = "all.tcl", .default_policy = "record", .baseline = "", .default_notfiles = "winDialog.test winMsgbox.test", .deps = &tk_test_deps },
};
const libtests_step = b.step("test-libraries", "library testsuites (thread, tclvfs) under the installed static shell per policy tier");
const libtests_step = b.step("test-libraries", "library testsuites under the installed static shell per policy tier (default: thread+tclvfs gate, tcllib+tklib record; tk excluded - see test-tk)");
for (lib_suites) |ls| {
const policy = b.option([]const u8, b.fmt("testpolicy-{s}", .{ls.name}), b.fmt("{s} testsuite policy: gate|record|skip (default {s})", .{ ls.name, ls.default_policy })) orelse ls.default_policy;
const lib_testargs = b.option([]const u8, b.fmt("testargs-{s}", .{ls.name}), b.fmt("extra tcltest/driver args for the {s} testsuite", .{ls.name})) orelse "";
const lib_notfiles = b.option([]const u8, b.fmt("testnotfiles-{s}", .{ls.name}), b.fmt("test-file glob patterns excluded from the {s} run (default \"{s}\")", .{ ls.name, ls.default_notfiles })) orelse ls.default_notfiles;
const is_tk = std.mem.eql(u8, ls.name, "tk");
const step = b.step(b.fmt("test-{s}", .{ls.name}), b.fmt("{s} testsuite under the installed static shell (policy: {s})", .{ ls.name, policy }));
libtests_step.dependOn(step);
if (!is_tk) libtests_step.dependOn(step);
if (std.mem.eql(u8, policy, "skip")) continue;
if (!std.mem.eql(u8, policy, "gate") and !std.mem.eql(u8, policy, "record")) {
std.debug.panic("-Dtestpolicy-{s}: unknown policy '{s}' (expected gate|record|skip)", .{ ls.name, policy });
@ -772,10 +954,13 @@ pub fn build(b: *std.Build) !void {
run.addArgs(&.{ "-driver", ls.driver });
if (std.mem.eql(u8, policy, "gate")) {
run.addArg("-baseline");
if (common.pathExists(b, ls.baseline)) {
if (ls.baseline.len != 0 and common.pathExists(b, ls.baseline)) {
run.addFileArg(b.path(ls.baseline));
} else {
run.addArg(common.replaceAll(b, b.pathFromRoot(ls.baseline), "\\", "/"));
//no tracked baseline (yet): pass the conventional name so the
//engine's missing-baseline message names the file to create
const conv = if (ls.baseline.len != 0) ls.baseline else b.fmt("expected_test_failures_{s}.txt", .{ls.name});
run.addArg(common.replaceAll(b, b.pathFromRoot(conv), "\\", "/"));
}
}
run.addArg("-logfile");
@ -820,6 +1005,11 @@ fn bootstrapMode(b: *std.Build) !void {
.{ "tcl", "tcl86" },
.{ "tclthread", "tclthread" },
.{ "tclvfs", "tclvfs" },
//G-100 dependency set
.{ "tk", "tk86" },
.{ "tklib", "tklib" },
.{ "tcllib", "tcllib" },
.{ "critcl", "critcl" },
};
var deps_pending = false;
for (source_dirs) |sd| {
@ -857,6 +1047,8 @@ fn bootstrapMode(b: *std.Build) !void {
"build_zlib86",
"build_tclthread86",
"build_tclvfs86",
"build_tk86",
"critcl_zig.config",
"expected_test_failures.txt",
"expected_test_failures_thread.txt",
"expected_test_failures_tclvfs.txt",
@ -874,7 +1066,8 @@ fn bootstrapMode(b: *std.Build) !void {
//bootstrap: stage, then drive the staged recipe (per-zig-version cache dir -
//object caches must never be shared across zig versions; forward-slash paths
//throughout: the prefix flows into -D macros as C string literals)
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries smoke)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "smoke" });
//default mirrors suite.tcl's build default (keep the two in step)
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries smoke tklib tcllib tcllibc)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "smoke", "tklib", "tcllib", "tcllibc" });
var cachever: []const u8 = builtin.zig_version_string;
cachever = common.replaceAll(b, cachever, "+", "_");
cachever = common.replaceAll(b, cachever, "/", "_");
@ -895,8 +1088,9 @@ fn bootstrapMode(b: *std.Build) !void {
nested.has_side_effects = true;
//cwd = staged build dir (suite.tcl parity)
nested.setCwd(.{ .cwd_relative = b.pathFromRoot(b.pathJoin(&.{ stage_rel, "build" })) });
//hermetic child shells (parity with suite.tcl)
for ([_][]const u8{ "TCLLIBPATH", "TCL_LIBRARY", "TK_LIBRARY" }) |ev| {
//hermetic child shells (parity with suite.tcl - same list as the per-step
//scrub, incl. the TCL<major>_<minor>_TM_PATH module roots)
for (common.tcl_env_vars) |ev| {
nested.removeEnvironmentVariable(ev);
}
nested.step.dependOn(stage_step);

64
src/buildsuites/suite_tcl86/build_common.zig

@ -53,11 +53,28 @@ pub fn replaceAll(b: *std.Build, input: []const u8, needle: []const u8, replacem
//never sets TCL_LIBRARY (built-shell runs execute the INSTALLED shell beside its
//installed lib tree, proving hermetic exe-relative resolution).
pub fn scrubTclEnv(run: *std.Build.Step.Run) void {
for ([_][]const u8{ "TCLLIBPATH", "TCL_LIBRARY", "TK_LIBRARY" }) |ev| {
for (tcl_env_vars) |ev| {
run.removeEnvironmentVariable(ev);
}
}
//The full set of tcl-environment vars a suite child must not inherit.
//TCL<major>_<minor>_TM_PATH is the one that bites quietly (G-100 finding
//2026-07-26): tm.tcl seeds its module roots from env(TCL8_6_TM_PATH) down
//through every lower minor of the major, and this dev machine points
//TCL8_6_TM_PATH at C:/repo/jn/shellspy/modules among others - so a suite shell
//silently preferred a repo-built sha1-2.0.4.tm over the tcllib 2.0.5 the suite
//had just installed. Any census taken through such a shell measures the
//machine, not the suite. Scrubbed for every generation, not just this suite's:
//the vars are cheap to clear and a copied suite inherits the protection.
pub const tcl_env_vars = [_][]const u8{
"TCLLIBPATH", "TCL_LIBRARY", "TK_LIBRARY",
"TCL8_0_TM_PATH", "TCL8_1_TM_PATH", "TCL8_2_TM_PATH",
"TCL8_3_TM_PATH", "TCL8_4_TM_PATH", "TCL8_5_TM_PATH",
"TCL8_6_TM_PATH", "TCL8_7_TM_PATH", "TCL9_0_TM_PATH",
"TCL9_1_TM_PATH",
};
//checkout uuid from a tree's manifest.uuid when the repo materializes it (a
//per-repo fossil 'manifest' setting; suite.tcl forces it on for staged
//checkouts), else "unrecorded" so provenance emission stays total.
@ -122,6 +139,51 @@ pub fn pkgIfneededVersion(b: *std.Build, pkgindex_from_root: []const u8, pkgname
std.debug.panic("no 'package ifneeded {s}' in {s}", .{ pkgname, pkgindex_from_root });
}
//tcllib-family installer trees carry 'package_version <v>' in
//support/installation/version.tcl (tklib and tcllib alike - both installers
//derive their default install dir name from it, and the suite passes it
//explicitly as -pkg-path so a machine-global tcl_pkgPath can never be chosen).
pub fn installerPackageVersion(b: *std.Build, tree_from_root: []const u8) []const u8 {
const data = readSourceFile(b, b.fmt("{s}/support/installation/version.tcl", .{tree_from_root}));
var lines = std.mem.splitScalar(u8, data, '\n');
while (lines.next()) |line| {
var toks = std.mem.tokenizeAny(u8, line, " \t\r");
const first = toks.next() orelse continue;
if (!std.mem.eql(u8, first, "package_version")) continue;
const ver = toks.next() orelse continue;
return b.dupe(ver);
}
std.debug.panic("no package_version in {s}/support/installation/version.tcl", .{tree_from_root});
}
//tcllib's critcl module files from its version.tcl declarations: the critcl_main
//module's files first, then every 'critcl' module's files (sak.tcl semantics).
//Returned paths are relative to the tcllib tree's modules/ dir. Tcl brace-list
//tokens are unwrapped by brace stripping (the declared file names contain no
//spaces).
pub fn tcllibCritclFiles(b: *std.Build, tcllib_from_root: []const u8) []const []const u8 {
const data = readSourceFile(b, b.fmt("{s}/support/installation/version.tcl", .{tcllib_from_root}));
var mains = std.array_list.Managed([]const u8).init(b.allocator);
var mods = std.array_list.Managed([]const u8).init(b.allocator);
var lines = std.mem.splitScalar(u8, data, '\n');
while (lines.next()) |line| {
var toks = std.mem.tokenizeAny(u8, line, " \t\r");
const first = toks.next() orelse continue;
const is_main = std.mem.eql(u8, first, "critcl_main");
if (!is_main and !std.mem.eql(u8, first, "critcl")) continue;
_ = toks.next() orelse continue; //module name
while (toks.next()) |tok| {
var f = tok;
f = std.mem.trim(u8, f, "{}");
if (f.len == 0) continue;
(if (is_main) &mains else &mods).append(b.dupe(f)) catch @panic("OOM");
}
}
if (mains.items.len == 0) @panic("no critcl_main declaration found in tcllib version.tcl");
mains.appendSlice(mods.items) catch @panic("OOM");
return mains.items;
}
//TCL_WIN_VERSION as the makefiles derive it:
//<dotversion>.<releaselevel>.<patchlevel with a/b/. stripped>, releaselevel
//0=alpha 1=beta 2=final (e.g 8.6 + 8.6.18 -> 8.6.2.8618)

260
src/buildsuites/suite_tcl86/build_tk86/build_tk86.zig

@ -0,0 +1,260 @@
const std = @import("std");
const common = @import("../build_common.zig");
//G-100: Tk 8.6 windows loadable extension (tk86t.dll) from stock core-8-6-branch
//sources, plus the tk script library installed to lib/tk8.6.
//
//DERIVED from the staged tk 8.6 win/makefile.vc + win/rules.vc - a re-derivation,
//not a port of the 9.0 build_tk.zig (the goal Context predicted this: the object
//lists and xlib arrangement differ). Differences from the 9.0 module, all
//makefile-authorised:
// - object inventory: 8.6 TKOBJS has no tkIcu / tkImgListFormat / tkImgSVGnano /
// tkPkgConfig, and no tkWinGDI / tkWinIco / tkWinSysTray. XLIBOBJS is the same
// 5-file emulation set but is a first-class makefile macro here (in 9.0 the
// xlib files had to be recovered by hand - the name-pattern extraction missed
// them).
// - NO tkPkgConfig.c in 8.6 -> no CFG_* configure-era defines at all (the 9.0
// module carries a full CFG_{RUNTIME,INSTALL}_* set for it).
// - dll naming: rules.vc's TCL_MAJOR_VERSION==8 arm gives TKLIBNAME
// tk86t.dll (SUFX 't' survives while TCL_VERSION <= 86); the 9.0 arm produces
// the tcl9tk90.dll form.
// - wish.exe.manifest version: 8.6's makefile substitutes @TK_WIN_VERSION@ with
// $(DOTVERSION).0.0 = "8.6.0.0" - NOT the tcl-style
// <dotversion>.<releaselevel>.<patchlevel> formula the 9.0 tree uses.
// - pkgIndex: the 8.6 install-binaries text provides ONLY 'Tk' (no lowercase
// 'tk' alias) and guards with 'package vsatisfies [package provide Tcl] 8.6.0'.
//
//Kept from the 9.0 module (same reasons):
// - USE_TCL_STUBS/USE_TCLOO_STUBS + link against the suite's libtclstub86.a:
// that IS the stock arrangement (rules.vc sets USE_STUBS_DEFS for every
// project except tcl itself), and it keeps the dll unbound to a particular
// tcl dll name so both the static and dynamic suite shells can load it.
// - tkMain.c compiled twice (makefile: tkMain.obj + tkMain2.obj). The ansi copy
// gets explicit -UUNICODE/-U_UNICODE so a driver-side UNICODE default could
// never turn it into a duplicate UNICODE compile (G-096 finding; verified
// 2026-07-26 that the pinned zig cc defines neither for x86_64-windows-gnu,
// so the guard is belt-and-braces and the stock ANSI/TCHAR arrangement of the
// 8.6 core is matched exactly).
// - tkUuid.h generated into the build cache (tkWindow.c #includes it) and
// consumed via an include-dir overlay added FIRST - source trees are never
// written (G-102 convention).
//win/makefile.vc TKOBJS, split by the directory each source actually lives in
//(verified against the staged tree - every name resolved to exactly one dir).
const generic_names = [_][]const u8{
"tkConsole", "tk3d", "tkArgv", "tkAtom", "tkBind",
"tkBitmap", "tkBusy", "tkButton", "tkCanvArc", "tkCanvBmap",
"tkCanvImg", "tkCanvLine", "tkCanvPoly", "tkCanvPs", "tkCanvText",
"tkCanvUtil", "tkCanvWind", "tkCanvas", "tkClipboard", "tkCmds",
"tkColor", "tkConfig", "tkCursor", "tkEntry", "tkError",
"tkEvent", "tkFileFilter", "tkFocus", "tkFont", "tkFrame",
"tkGC", "tkGeometry", "tkGet", "tkGrab", "tkGrid",
"tkImage", "tkImgBmap", "tkImgGIF", "tkImgPNG", "tkImgPPM",
"tkImgPhoto", "tkImgPhInstance", "tkImgUtil", "tkListbox", "tkMacWinMenu",
"tkMenu", "tkMenubutton", "tkMenuDraw", "tkMessage", "tkPanedWindow",
"tkObj", "tkOldConfig", "tkOption", "tkPack", "tkPlace",
"tkPointer", "tkRectOval", "tkScale", "tkScrollbar", "tkSelect",
"tkStyle", "tkText", "tkTextBTree", "tkTextDisp", "tkTextImage",
"tkTextIndex", "tkTextMark", "tkTextTag", "tkTextWind", "tkTrig",
"tkUndo", "tkUtil", "tkVisual", "tkStubInit", "tkWindow",
};
const unix_names = [_][]const u8{ "tkUnixMenubu", "tkUnixScale" };
const xlib_names = [_][]const u8{ "xcolors", "xdraw", "xgc", "ximage", "xutil" };
const win_names = [_][]const u8{
"tkWin3d", "tkWin32Dll", "tkWinButton", "tkWinClipboard", "tkWinColor",
"tkWinConfig", "tkWinCursor", "tkWinDialog", "tkWinDraw", "tkWinEmbed",
"tkWinFont", "tkWinImage", "tkWinInit", "tkWinKey", "tkWinMenu",
"tkWinPixmap", "tkWinPointer", "tkWinRegion", "tkWinScrlbr", "tkWinSend",
"tkWinSendCom", "tkWinWindow", "tkWinWm", "tkWinX", "stubs",
"ttkWinMonitor", "ttkWinTheme", "ttkWinXPTheme",
};
const ttk_names = [_][]const u8{
"ttkBlink", "ttkButton", "ttkCache", "ttkClamTheme", "ttkClassicTheme",
"ttkDefaultTheme", "ttkElements", "ttkEntry", "ttkFrame", "ttkImage",
"ttkInit", "ttkLabel", "ttkLayout", "ttkManager", "ttkNotebook",
"ttkPanedwindow", "ttkProgress", "ttkScale", "ttkScrollbar", "ttkScroll",
"ttkSeparator", "ttkSquare", "ttkState", "ttkTagSet", "ttkTheme",
"ttkTrace", "ttkTrack", "ttkTreeview", "ttkWidget", "ttkStubInit",
};
pub const TkBuild = struct {
lib: *std.Build.Step.Compile,
pkgidx: std.Build.LazyPath,
patchlevel: []const u8, //e.g "8.6.17" (tk.h TK_PATCH_LEVEL)
libdir: []const u8, //install dir name, e.g "tk8.6"
dll_file: []const u8, //e.g "tk86t.dll"
};
pub fn build_tk86(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !TkBuild {
//-- version facts from the staged tree (never hardcoded)
const tkh_content = common.readSourceFile(b, subdir ++ "/generic/tk.h");
const tk_h_version = common.parseDefineString(tkh_content, "TK_VERSION") orelse @panic("TK_VERSION not found in tk.h");
const tk_h_patchlevel = common.parseDefineString(tkh_content, "TK_PATCH_LEVEL") orelse @panic("TK_PATCH_LEVEL not found in tk.h");
if (!std.mem.eql(u8, tk_h_version, "8.6")) {
std.debug.panic("staged tk tree is {s} but this recipe expresses Tk 8.6 (sources.config tk ref / dir mismatch?)", .{tk_h_version});
}
const tk_nodot = "86";
const dll_name = "tk" ++ tk_nodot ++ "t"; //rules.vc TKLIBNAME, TCL_MAJOR_VERSION==8 arm
const dll_file = dll_name ++ ".dll";
const script_dir_rel = "lib/tk8.6"; //rules.vc SCRIPT_INSTALL_DIR for DOING_TK
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = dll_name,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
//-- flag classes (rules.vc pkgcflags for PROJECT=tk: cflags + INCLUDES +
//TCL_DEFINES + PRJ_DEFINES + OPTDEFINES + USE_STUBS_DEFS + /DBUILD_tk)
var flags = std.array_list.Managed([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{
"-Wall",
"-O3",
"-fomit-frame-pointer",
"-finput-charset=UTF-8",
//the tcl 8.6 recipe's finding, kept here: clang UBSan traps must stay
//fully off on windows
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
//project defines: makefile.vc PRJ_DEFINES (/DBUILD_ttk + CONFIG_DEFS;
//inline=__inline and the _CRT_*_NO_DEPRECATE pair are MSVC-isms)
"-DBUILD_tk",
"-DBUILD_ttk",
"-DSTDC_HEADERS=1",
"-DHAVE_SYS_TYPES_H=1",
"-DHAVE_SYS_STAT_H=1",
"-DHAVE_STDLIB_H=1",
"-DHAVE_STRING_H=1",
"-DHAVE_MEMORY_H=1",
"-DHAVE_STRINGS_H=1",
"-DHAVE_UXTHEME_H=1", //makefile default (OPTS=noxp turns it off)
//rules.vc USE_STUBS_DEFS for a non-tcl project
"-DUSE_TCL_STUBS=1",
"-DUSE_TCLOO_STUBS=1",
//OPTDEFINES equivalents, matching the flag set the 8.6 core was built with
"-DTCL_THREADS=1",
"-DNDEBUG=1",
"-DTCL_CFG_OPTIMIZED=1",
"-DTCL_CFG_DO64BIT=1",
"-DHAVE_NO_SEH=1", //gcc-class: no __try/__except
});
var sources = std.array_list.Managed([]const u8).init(b.allocator);
inline for (generic_names) |n| try sources.append(subdir ++ "/generic/" ++ n ++ ".c");
inline for (unix_names) |n| try sources.append(subdir ++ "/unix/" ++ n ++ ".c");
inline for (xlib_names) |n| try sources.append(subdir ++ "/xlib/" ++ n ++ ".c");
inline for (win_names) |n| try sources.append(subdir ++ "/win/" ++ n ++ ".c");
inline for (ttk_names) |n| try sources.append(subdir ++ "/generic/ttk/" ++ n ++ ".c");
//tkUuid.h overlay: makefile.vc concatenates win/tkUuid.h.in + the checkout's
//manifest.uuid; wrapfiletofile reproduces exactly that.
const wrapfilecmd = b.addExecutable(.{
.name = "wrapfiletofile",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/wrapfiletofile.zig"),
.target = b.graph.host,
}),
});
const uuidcmd_step = b.addRunArtifact(wrapfilecmd);
uuidcmd_step.addArgs(&.{
"-prefix",
"#define TK_VERSION_UUID \\",
"-prefixnl",
"1",
"-input",
});
uuidcmd_step.addFileArg(b.path(subdir ++ "/manifest.uuid"));
uuidcmd_step.addArg("-output");
const tkuuid_file = uuidcmd_step.addOutputFileArg("tkUuid.h");
lib.root_module.addIncludePath(tkuuid_file.dirname()); //overlay first
lib.root_module.addIncludePath(b.path(subdir ++ "/generic"));
lib.root_module.addIncludePath(b.path(subdir ++ "/generic/ttk"));
lib.root_module.addIncludePath(b.path(subdir ++ "/win"));
lib.root_module.addIncludePath(b.path(subdir ++ "/xlib")); //PRJ_INCLUDES
lib.root_module.addIncludePath(b.path(subdir ++ "/bitmaps")); //PRJ_INCLUDES
lib.root_module.addIncludePath(b.path(tcldir ++ "/generic"));
lib.root_module.addIncludePath(b.path(tcldir ++ "/win"));
lib.root_module.addCSourceFiles(.{
.files = sources.items,
.flags = flags.items,
});
//tkMain.c twice - ansi (forced) + UNICODE 'tkMain2' (makefile.vc parity)
var mainansi_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer mainansi_flags.deinit();
try mainansi_flags.appendSlice(flags.items);
try mainansi_flags.appendSlice(&.{ "-UUNICODE", "-U_UNICODE" });
lib.root_module.addCSourceFile(.{
.file = b.path(subdir ++ "/generic/tkMain.c"),
.flags = mainansi_flags.items,
});
var mainw_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer mainw_flags.deinit();
try mainw_flags.appendSlice(flags.items);
try mainw_flags.appendSlice(&.{ "-DUNICODE", "-D_UNICODE" });
lib.root_module.addCSourceFile(.{
.file = b.path(subdir ++ "/generic/tkMain.c"),
.flags = mainw_flags.items,
});
//wish.exe.manifest overlay: win/rc/tk.rc embeds it as RT_MANIFEST (that is
//what enables comctl32 v6 themed scrollbars in the DLL), and RT_MANIFEST
//file references resolve through the rc include paths. 8.6's makefile
//substitutes @TK_WIN_VERSION@ with $(DOTVERSION).0.0.
const tk_win_version = b.fmt("{s}.0.0", .{tk_h_version});
const wish_manifest_content = common.replaceAll(b, common.replaceAll(b, common.readSourceFile(b, subdir ++ "/win/wish.exe.manifest.in"), "@TK_WIN_VERSION@", tk_win_version), "@MACHINE@", "AMD64");
const wf_tkrc = b.addWriteFiles();
_ = wf_tkrc.add("wish.exe.manifest", wish_manifest_content);
lib.root_module.addWin32ResourceFile(.{
.file = b.path(subdir ++ "/win/rc/tk.rc"),
//TCL_THREADS drives the rc's OriginalFilename suffix (tk86t.dll)
.flags = &.{"-DTCL_THREADS=1"},
.include_paths = &.{
wf_tkrc.getDirectory(),
b.path(subdir ++ "/win"),
b.path(subdir ++ "/win/rc"),
b.path(subdir ++ "/generic"),
b.path(tcldir ++ "/generic"),
},
});
lib.root_module.linkLibrary(stublib);
lib.root_module.link_libc = true;
//rules.vc winlibs + makefile.vc PRJ_LIBS, plus the OLE/dialog/IME imports the
//win sources pull in (tkWinSendCom, tkWinDialog, tkWinX)
inline for (.{ "netapi32", "gdi32", "user32", "userenv", "winspool", "shell32", "ole32", "oleaut32", "uuid", "comctl32", "comdlg32", "imm32", "uxtheme", "advapi32", "kernel32" }) |l| {
lib.root_module.linkSystemLibrary(l, .{});
}
b.installArtifact(lib); //bin/tk86t.dll (+ tk86t.lib import lib -> lib/)
//tk script library -> <prefix>/lib/tk8.6 (whole-tree copy: stock splits the
//demos out to a separate dir, this keeps them inside - same simplification
//the tcl-side library install makes)
const install_tk_library = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library"),
.install_dir = .prefix,
.install_subdir = script_dir_rel,
});
b.getInstallStep().dependOn(&install_tk_library.step);
//pkgIndex.tcl per makefile.vc install-binaries (windows arm of its two-branch
//text - the unix DISPLAY branch cannot apply to a windows-only suite;
//'file normalize' added as in the 9.0 recipe so the load path is cwd-proof).
const wf_pkgidx = b.addWriteFiles();
const pkgidx = wf_pkgidx.add("pkgIndex.tcl", b.fmt(
\\if {{![package vsatisfies [package provide Tcl] 8.6.0]}} return
\\package ifneeded Tk {s} [list load [file normalize [file join $dir .. .. bin {s}]]]
\\
, .{ tk_h_patchlevel, dll_file }));
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, script_dir_rel ++ "/pkgIndex.tcl");
b.getInstallStep().dependOn(&pkgidx_install.step);
return .{ .lib = lib, .pkgidx = pkgidx, .patchlevel = tk_h_patchlevel, .libdir = "tk8.6", .dll_file = dll_file };
}

36
src/buildsuites/suite_tcl86/critcl_zig.config

@ -0,0 +1,36 @@
# suite_tcl86 critcl configuration: zig cc as the C toolchain (G-100).
# Forked from suite_tcl90/critcl_zig.config - generation-neutral content: critcl
# compiles against whichever tcl headers/stubs the DRIVING shell reports, so the
# 8.6 suite needs no flag differences here. 'zig' is resolved from PATH - the
# suite recipe prepends the pinned bin/tools zig dir for the critcl invocation.
# Target is the native windows-gnu triple (matches the suite's tcl runtime ABI).
#
# The settings live under an EXPLICIT target, win32-x86_64-zig, and the recipe
# invokes critcl with '-target win32-x86_64-zig': critcl otherwise chooses its
# build target by PROBING the environment for compilers, which made the selection
# depend on what happened to be on PATH (a visible gcc landed on the config
# defaults; a scrubbed environment fell back to critcl's builtin msvc 'cl'
# target and never used these settings - suite_tcl90 G-102 finding).
platform win32-x86_64
win32-x86_64-zig platform win32-x86_64
win32-x86_64-zig compile zig cc -target x86_64-windows-gnu -c -fPIC
win32-x86_64-zig version zig version
win32-x86_64-zig link zig cc -target x86_64-windows-gnu -shared
win32-x86_64-zig include -I
win32-x86_64-zig libinclude -L
win32-x86_64-zig object .o
win32-x86_64-zig output -o [list $outfile]
win32-x86_64-zig optimize -O2
win32-x86_64-zig noassert -DNDEBUG
win32-x86_64-zig tclstubs -DUSE_TCL_STUBS
win32-x86_64-zig tkstubs -DUSE_TK_STUBS
win32-x86_64-zig debug_symbols -g
win32-x86_64-zig strip -Wl,-s
win32-x86_64-zig link_debug
win32-x86_64-zig link_release
win32-x86_64-zig embed_manifest
win32-x86_64-zig ldoutput
win32-x86_64-zig link_preload
win32-x86_64-zig sharedlibext .dll

23
src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt

@ -15,12 +15,25 @@
# against the retired external FTP host - the same 1.4.2-era entries that
# suite_tcl90's tclvfs baseline note records from its 2026-07-21 capture.
# -- external-service dependency: both tests reach a public FTP server
# -- external-service dependency: these tests reach a public FTP server
# (ftp://ftp.tcl.tk) the Tcl project retired years ago, so they fail on any
# modern machine/network regardless of the build (vfsUrl-1.2 asserts
# 'file exists ftp://ftp.tcl.tk' == 1; vfsUrl-2.1 copies a file from it). Not a
# suite defect - the tclvfs URL handlers themselves are exercised by the passing
# vfsUrl mount/parse cases. (The file's other ftp cases are gated behind the
# 'ftp' constraint and skip; these two are not constraint-gated.)
# 'file exists ftp://ftp.tcl.tk' == 1; vfsUrl-2.1 copies a file from it;
# vfsFtp-1.1..1.3 mount/list/stat that host). Not a suite defect - the tclvfs
# URL/FTP handlers themselves are exercised by the passing mount/parse cases.
#
# G-100 EXTENSION 2026-07-26: the vfsFtp entries joined the baseline when the
# suite grew tcllib. They are gated behind the 'ftp' constraint, which needs
# tcllib's ftp package - absent at the G-099 capture (3 constraint skips),
# satisfied once 'suite.tcl build' installs tcllib 2.0, so they now RUN and
# reach the retired host. Skips accordingly dropped 8 -> 5.
#
# WHICH members of this family fail is network-timing dependent (a connect that
# times out vs one refused fast can flip a case between fail and error-then-fail
# across runs), so the gate is expected to report some of these as 'stale
# baseline' entries on any given run - stale entries are noted, never fatal.
vfsUrl-1.2
vfsUrl-2.1
vfsFtp-1.1
vfsFtp-1.2
vfsFtp-1.3

24
src/buildsuites/suite_tcl86/sources.config

@ -27,14 +27,34 @@
# (2.8.13, 2026-04-30) - selection recorded in goals G-099.
# - tclvfs keeps TRUNK (1.4.2 era): the forTcl9 branch (1.5.0) is for tcl9
# suites only (user decision 2026-07-22, recorded in suite_tcl90).
#
# G-100 dependency-set notes:
# - tk rides core-8-6-branch (the 8.6 maintenance line, pairing with the 8.6
# runtime) into dir tk86 - the 9.0 recipe's tk9 dir is version-named the
# same way. The object lists/xlib arrangement differ from 9.0, so
# build_tk86 is a re-derivation from the 8.6 win/makefile.vc, not a reuse.
# - tklib/tcllib/critcl are GENERATION-NEUTRAL upstreams: the same trunk refs
# (and the same zon pins) as suite_tcl90 - what differs is the shell that
# runs their installers and the critcl target that builds tcllibc. Both
# libraries install per-generation (tcl8 vs tcl9 tcllibc side by side is the
# established punk kit payload shape).
description Zig-built Tcl 8.6 windows runtime (core-8-6-branch): tclsh86ts/tclsh86t shells + tcl86t.dll + installed lib tree, thread 2.8 + tclvfs, core-test gate (G-099 arc)
description Zig-built Tcl 8.6 windows runtime (core-8-6-branch): tclsh86ts/tclsh86t shells + tcl86t.dll + installed lib tree, thread 2.8 + tclvfs + Tk 8.6 + tklib/tcllib(+tcllibc), core-test gate (G-099/G-100 arc)
product tclsh86ts.exe static shell + tclsh86t.exe/tcl86t.dll dynamic pair with installed lib/tcl8.6 tree + tm modules (no zipfs in 8.6 - no self-contained szip runtime; kit containment is G-101 territory)
product thread 2.8.13 (thread2813t.dll) and tclvfs 1.4.2 (vfs142t.dll) + registry/dde package dlls, all loadable in both shells
product core + thread + tclvfs testsuite gates vs tracked expected_test_failures baselines (parsed all.tcl totals; first censuses in flight - see README)
product Tk 8.6.17 (tk86t.dll + lib/tk8.6 script library) with tklib 0.9 and tcllib 2.0 installed by their own installers, plus tcllibc critcl accelerators built with zig cc
product core + thread + tclvfs testsuite gates vs tracked expected_test_failures baselines (parsed all.tcl totals), tcllib/tklib record tiers and an opt-in test-tk
doc README.md
zigpin bin/tools/zig-x86_64-windows-0.16.0/zig.exe
source tcl fossil https://core.tcl-lang.org/tcl core-8-6-branch tcl86
source tclthread fossil https://core.tcl-lang.org/thread thread-2-8-branch tclthread
source tclvfs fossil https://core.tcl-lang.org/tclvfs trunk tclvfs
# G-100: Tk + companion script libraries. tklib is pure-tcl (installed by its own
# installer where that matters); tcllib's accelerators (tcllibc) are built via critcl.
source tk fossil https://core.tcl-lang.org/tk core-8-6-branch tk86
source tklib fossil https://core.tcl-lang.org/tklib trunk tklib
source tcllib fossil https://core.tcl-lang.org/tcllib trunk tcllib
source critcl git https://github.com/andreas-kupries/critcl 3.3.1 critcl

44
src/buildsuites/suite_tcl86/suite.tcl

@ -24,11 +24,16 @@
# -seedfossils <dir> read-only seed collection for missing clones (default ~/.fossils; "" disables)
# -all <0|1> with 'clean': reserved (the clone store lives outside the stage)
# -steps <list> zig build steps. 'build' default: {install install-libraries
# smoke} (runtimes + companions + script library + smokes).
# smoke tklib tcllib tcllibc} (runtimes + companions + Tk +
# script library + smokes, then the G-100 dependency set:
# tklib/tcllib installed by their own installers and the
# critcl-built tcllibc accelerators).
# 'test' default: {test-gate} (the 8.6 core suite gate vs the
# tracked expected_test_failures baseline; first census via
# -zigargs {-Dtestmode=record}). Also: test-thread, test-tclvfs,
# test-libraries (G-107-pattern policy tiers).
# test-libraries (G-107-pattern policy tiers), test-tcllib,
# test-tklib, and the OPT-IN test-tk (full Tk suite: maps
# windows on the interactive desktop, tens of minutes).
# -zigargs <list> extra args appended to the zig build invocation
#
#The build area lives entirely under src/buildsuites/_build (VCS-ignored). Fossil
@ -119,7 +124,7 @@ array set opt {
-optimize ReleaseFast
-tclbranch {}
-refresh 0
-steps {install install-libraries smoke}
-steps {install install-libraries smoke tklib tcllib tcllibc}
-testargs {}
-zigargs {}
-repofolder {}
@ -170,9 +175,15 @@ if {$opt(-repofolder) ne ""} {
#Hermetic child shells: the suite-built tclsh must not see the user's machine-level
#package paths (TCLLIBPATH/TCL_LIBRARY misdirect installer paths and risk
#false-positive smokes). The zig recipe additionally scrubs these per built-shell
#Run step (suite_tcl90 G-102 convention, kept for the 8.6 recipe).
foreach ev {TCLLIBPATH TCL_LIBRARY TK_LIBRARY} {
#false-positive smokes; TCL<major>_<minor>_TM_PATH seeds tm.tcl's module roots and
#is the quiet one - this machine points TCL8_6_TM_PATH at repo/foreign module trees,
#which shadowed suite-installed tcllib packages until the G-100 work caught it).
#The zig recipe scrubs the same list per built-shell Run step (build_common.zig
#tcl_env_vars - keep the two lists in step).
foreach ev {TCLLIBPATH TCL_LIBRARY TK_LIBRARY
TCL8_0_TM_PATH TCL8_1_TM_PATH TCL8_2_TM_PATH TCL8_3_TM_PATH
TCL8_4_TM_PATH TCL8_5_TM_PATH TCL8_6_TM_PATH TCL8_7_TM_PATH
TCL9_0_TM_PATH TCL9_1_TM_PATH} {
if {[info exists env($ev)]} {
log "unsetting inherited \$env($ev) for hermetic child shells"
unset env($ev)
@ -276,19 +287,33 @@ proc fossil_source {name url branch dir} {
cd $savedpwd
}
proc git_source {name url ref dir} {
#clone (or leave) a git checkout pinned at ref (critcl - the recipe's tcllibc
#step consumes it from the stage; suite_tcl90 convention)
global opt
if {![file isdirectory [file join $dir .git]]} {
log "cloning $name (git) -> $dir"
run git clone --quiet $url $dir
} elseif {$opt(-refresh)} {
run git -C $dir fetch --quiet
}
run git -C $dir checkout --quiet $ref
}
#-- stage the recipe ------------------------------------------------------
#Per-item sync (not a wholesale delete of the build dir): zig cache dirs live in
#the build dir and are kept (per-zig-version; windows can hold transient locks on
#recently-used cache trees).
#recently-used cache trees). Item list mirrors the zig bootstrap mode's
#recipe_items (build86.zig bootstrapMode).
log "staging recipe -> $builddir"
file mkdir $builddir
foreach item {build86.zig build_common.zig build_zlib86 build_tclthread86 build_tclvfs86 expected_test_failures.txt expected_test_failures_thread.txt expected_test_failures_tclvfs.txt src tools} {
foreach item {build86.zig build_common.zig build_zlib86 build_tclthread86 build_tclvfs86 build_tk86 critcl_zig.config expected_test_failures.txt expected_test_failures_thread.txt expected_test_failures_tclvfs.txt src tools} {
file delete -force [file join $builddir $item]
file copy [file join $suiteroot $item] $builddir
}
#-- sources ---------------------------------------------------------------
foreach required {tcl tclthread tclvfs} {
foreach required {tcl tclthread tclvfs tk tklib tcllib critcl} {
if {![dict exists $sources $required]} {
fail "sources.config: missing required source record '$required'"
}
@ -301,6 +326,7 @@ dict for {name s} $sources {
}
switch -- [dict get $s kind] {
fossil {fossil_source $name [dict get $s url] $ref [file join $stage [dict get $s dir]]}
git {git_source $name [dict get $s url] $ref [file join $stage [dict get $s dir]]}
}
}

50
src/buildsuites/suite_tcl86/tools/accel_bench.tcl

@ -0,0 +1,50 @@
#accel_bench.tcl (G-100): tcllibc accelerator evidence under a suite-built shell.
#Hashes the same payload twice through tcllib's sha1 - once on the critcl
#accelerator (tcllibc), once on the pure-Tcl implementation - asserts the two
#digests match and prints the timings plus the speedup ratio. This is the
#punkcheck-relevant comparison (punkcheck hashing is the workload that made the
#accelerators worth building).
#
#Both implementations are made available with ::sha1::LoadAccelerator (the
#package itself stops loading at the FIRST working accelerator, so 'tcl' is
#normally not registered when critcl won) and selected with ::sha1::SwitchTo.
#
#args: ?-mb <payload megabytes>? (default 4)
proc fail {msg} {puts stderr "accel_bench FAIL: $msg"; flush stderr; exit 1}
set mb 4
for {set i 0} {$i < [llength $argv]} {incr i} {
switch -- [lindex $argv $i] {
-mb {incr i; set mb [lindex $argv $i]}
default {fail "unknown argument: [lindex $argv $i]"}
}
}
if {[catch {package require sha1} sha1ver]} {fail "package require sha1: $sha1ver"}
if {[catch {package require tcllibc} tcllibcver]} {fail "package require tcllibc: $tcllibcver"}
if {![::sha1::LoadAccelerator critcl]} {fail "sha1 critcl accelerator not available (tcllibc $tcllibcver loaded but sha1c missing?)"}
if {![::sha1::LoadAccelerator tcl]} {fail "sha1 pure-tcl implementation not available"}
#payload: incompressible-ish repeated block, built once, hashed by both
set block [string repeat "punkshell-suite_tcl86-accel-bench-0123456789abcdef" 1024]
set data ""
set target [expr {$mb * 1024 * 1024}]
while {[string length $data] < $target} {append data $block}
set bytes [string length $data]
::sha1::SwitchTo critcl
set t_critcl [lindex [time {set d_critcl [::sha1::sha1 -hex -- $data]}] 0]
::sha1::SwitchTo tcl
set t_tcl [lindex [time {set d_tcl [::sha1::sha1 -hex -- $data]}] 0]
if {$d_critcl ne $d_tcl} {fail "digest mismatch: critcl $d_critcl vs pure-tcl $d_tcl"}
if {$t_critcl <= 0} {set t_critcl 1}
set speedup [format %.1f [expr {double($t_tcl) / double($t_critcl)}]]
set ms_critcl [format %.1f [expr {$t_critcl / 1000.0}]]
set ms_tcl [format %.1f [expr {$t_tcl / 1000.0}]]
puts "accel_bench OK: sha1 over $bytes bytes - critcl ${ms_critcl}ms vs pure-tcl ${ms_tcl}ms = ${speedup}x (digests identical, sha1 $sha1ver, tcllibc $tcllibcver)"
flush stdout
exit 0

64
src/buildsuites/suite_tcl86/tools/tk_smoke.tcl

@ -0,0 +1,64 @@
#tk_smoke.tcl (G-100): Tk window-station smoke under a suite-built shell.
#Goes beyond 'package require Tk' (a load-only check passes even when no usable
#display exists): it maps a real toplevel with a widget in it, forces a display
#update, then destroys it - the create/destroy-window verification the G-100
#acceptance asks for, run on every build rather than once by hand.
#
#args: ?-expect <tk patchlevel>?
#Exits 1 with a 'tk_smoke FAIL:' line on any failure; prints one evidence line
#on success (version, screen geometry/depth, windowingsystem, widget classes).
proc fail {msg} {puts stderr "tk_smoke FAIL: $msg"; flush stderr; exit 1}
set expect {}
for {set i 0} {$i < [llength $argv]} {incr i} {
switch -- [lindex $argv $i] {
-expect {incr i; set expect [lindex $argv $i]}
default {fail "unknown argument: [lindex $argv $i]"}
}
}
if {[catch {package require Tk} tkver]} {fail "package require Tk: $tkver"}
if {$expect ne "" && $tkver ne $expect} {fail "Tk version $tkver, expected $expect"}
if {[catch {package require Ttk} ttkver]} {set ttkver "-"}
#the root window itself: geometry facts prove a real window station/display
if {[catch {
wm title . "tk_smoke"
wm geometry . 320x120+80+80
set screen "[winfo screenwidth .]x[winfo screenheight .] depth [winfo screendepth .]"
set wsys [tk windowingsystem]
} err]} {fail "root window setup: $err"}
#create: a classic widget, a themed widget and a child toplevel
if {[catch {
label .l -text "tk_smoke"
pack .l
ttk::button .b -text "ok"
pack .b
toplevel .t
wm title .t "tk_smoke child"
wm geometry .t 200x80+420+80
label .t.l -text "child"
pack .t.l
update
} err]} {fail "widget create/map: $err"}
foreach {label w} {root . classic .l themed .b child .t} {
if {![winfo ismapped $w]} {fail "window not mapped after update: $label ($w)"}
}
set classes [list [winfo class .l] [winfo class .b] [winfo class .t]]
#destroy: the child toplevel first, then every root child - a clean teardown
if {[catch {
destroy .t
update
if {[winfo exists .t]} {fail "child toplevel survived destroy"}
destroy .l .b
update
if {[llength [winfo children .]]} {fail "root children survived destroy: [winfo children .]"}
} err]} {fail "widget destroy: $err"}
puts "tk_smoke OK: Tk $tkver (Ttk $ttkver) windowingsystem $wsys screen $screen widgets [join $classes /] create+map+destroy clean"
flush stdout
exit 0
Loading…
Cancel
Save