Browse Source

G-099 achieved: suite_tcl86 zig buildsuite for Tcl 8.6 (runtime, thread 2.8, tclvfs, core-test gate)

Builds a Tcl 8.6.18 windows runtime from core-8-6-branch sources with the
pinned zig 0.16.0 toolchain only (no MS toolchain), forked from suite_tcl90
(duplication-with-a-note posture). Products (stock threaded-release naming -
rules.vc keeps the 't' SUFX on 8.6):

  bin/tcl86t.dll + tcl86t.lib   shared core (static zlib compiled in; no zlib1.dll)
  bin/tclsh86ts.exe             static shell (kit-class; dde/registry static)
  bin/tclsh86t.exe              dynamic shell against the dll
  lib/libtclstub86.a            stub lib for extensions
  lib/tcl8.6/...                script library tree (dde/reg dlls inside)
  lib/tcl8/{8.5,8.6}/...        tm modules; lib/thread2.8.13, lib/vfs1.4.2

Companions: thread 2.8.13 (thread2813t.dll, TEA installed shape from the
checkout's own pkgIndex.tcl.in) and tclvfs 1.4.2 (vfs142t.dll) build and load;
registry 1.3.5 / dde 1.4.6. No zipfs in 8.6 - exe+dll+on-disk lib tree only,
resolved hermetically (exe/dll-relative, TCLLIBPATH/TCL_LIBRARY unset in all
child runs).

Recipe: build86.zig + build_common.zig + build_zlib86/build_tclthread86/
build_tclvfs86 helpers; tools/ and src/main.zig copied verbatim (suite-agnostic).
Two load-bearing 8.6 findings recorded in the README DERIVATION and recipe
comments: MP_FIXED_CUTOFFS (the in-core libtommath ships no bn_cutoffs.c) and
TCL_BROKEN_MAINARGS + console subsystem for the gcc-class main() entry.

Test gates (suite.tcl test -> zig build test-gate/test-libraries) parse all.tcl
totals (exit codes untrusted) and gate against tracked dispositioned baselines.
First census + two-consecutive-run deterministic PASS (machine SuperBee):
  core   46525 run / 2 failed (filename-16.12/16.13 admin-share self-globbing)
  thread   142 run / 0 failed (empty baseline)
  tclvfs    51 run / 2 failed (vfsUrl-1.2/2.1 external ftp.tcl.tk dependency)

Goal flipped to achieved 2026-07-26 and archived; reference sweep updated G-005,
G-100 (now unblocked), G-101 and ARCHITECTURE.md. No project-version bump
(arm's-length build tooling, not shipped shell behaviour).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
a20a64a2a1
  1. 4
      ARCHITECTURE.md
  2. 4
      GOALS-archive.md
  3. 4
      GOALS.md
  4. 2
      goals/G-005-zig-build-infrastructure.md
  5. 17
      goals/G-100-suite-tcl86-tk-tcllib.md
  6. 4
      goals/G-101-tcl86-kit-container-strategy.md
  7. 76
      goals/archive/G-099-suite-tcl86-buildsuite.md
  8. 8
      src/buildsuites/AGENTS.md
  9. 60
      src/buildsuites/suite_tcl86/README.md
  10. 7
      src/buildsuites/suite_tcl86/build.zig
  11. 915
      src/buildsuites/suite_tcl86/build86.zig
  12. 141
      src/buildsuites/suite_tcl86/build_common.zig
  13. 137
      src/buildsuites/suite_tcl86/build_tclthread86/build_tclthread86.zig
  14. 161
      src/buildsuites/suite_tcl86/build_tclvfs86/build_tclvfs86.zig
  15. 50
      src/buildsuites/suite_tcl86/build_zlib86/build_zlib86.zig
  16. 23
      src/buildsuites/suite_tcl86/expected_test_failures.txt
  17. 26
      src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt
  18. 13
      src/buildsuites/suite_tcl86/expected_test_failures_thread.txt
  19. 8
      src/buildsuites/suite_tcl86/sources.config
  20. 4
      src/buildsuites/suite_tcl86/src/main.zig
  21. 22
      src/buildsuites/suite_tcl86/suite.tcl
  22. 42
      src/buildsuites/suite_tcl86/tools/pkg_smoke.tcl
  23. 173
      src/buildsuites/suite_tcl86/tools/stagetree.zig
  24. 26
      src/buildsuites/suite_tcl86/tools/suite_smoke.tcl
  25. 218
      src/buildsuites/suite_tcl86/tools/test_gate.tcl
  26. 92
      src/buildsuites/suite_tcl86/tools/wrapfiletofile.zig

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. The 8.6 family and kit container strategy are 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, 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".
- **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, G-100, G-101.
- 8.6 buildsuite family: G-099 (core runtime, achieved 2026-07-26), G-100 (dependency set), G-101 (kit container).
## Doc map - which lane holds what

4
GOALS-archive.md

@ -186,3 +186,7 @@ Acceptance: list shows suite_tcl90 (and any copied tree) with descriptions sourc
### G-121 [achieved 2026-07-26] make.tcl bakelist + selective bake: list configured kits, bake by name → detail: goals/archive/G-121-bakelist-selective-bake.md
Scope: src/make.tcl (bake @values + new bakelist subcommand; SUBOPTS/SUMMARIES/HELPTEXTS/SUBGROUPS, help/workflow text); src/runtime/mapvfs.config (as read; todo-comment retirement); src/tests/shell/testsuites/punkexe/ (bakelist characterization if feasible); layout make.tcl copies via established sync channels
Acceptance: `make.tcl bakelist` under a plain tclsh and under a punk kit executable lists every kit output configured in the runtime mapping with name, kit type, runtime (present/missing), vfs folder and a deployed-state indication (bin copy absent/current/stale vs the build product), and `bakelist <kitname>` filters to the named entries; `make.tcl bake <kitname ...>` builds and deploys only the named kit(s) - a recorded verification shows the named kit's artifacts refreshing while other kits' _build/bin artifacts and punkcheck records are untouched - and an unknown name errors without building, listing the configured names; bare `make.tcl bake` processes the same kit set as before the change; help/workflow text documents both per the update contract and the mapvfs.config todo comment is retired to point here; a piped bakelist characterization test lands under punkexe if feasible (else the infeasibility is recorded here); project version minor-bumped with a CHANGELOG entry.
### 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.

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-099 [active] suite_tcl86: zig buildsuite for core-8-6-branch (runtime, thread, tclvfs, core-test gate)
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)
Detail: goals/G-099-suite-tcl86-buildsuite.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

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-102/G-103/G-104/G-107, live G-099/G-100/G-105/G-108/
system - achieved G-096/G-098/G-099/G-102/G-103/G-104/G-107, live G-100/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

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

@ -8,7 +8,11 @@ Acceptance: from the pinned zig and fetched sources the suite produces a Tk 8.6
## 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).
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):
@ -40,3 +44,14 @@ G-096 (achieved 2026-07-20) remainder closed and punk905_beta trial concludes.
- 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).

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

@ -37,7 +37,9 @@ 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), and only worth activating once G-099/G-100 land.
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.
## Notes

76
goals/G-099-suite-tcl86-buildsuite.md → goals/archive/G-099-suite-tcl86-buildsuite.md

@ -1,6 +1,6 @@
# G-099 suite_tcl86: zig buildsuite for core-8-6-branch (runtime, thread, tclvfs, core-test gate)
Status: active
Status: achieved 2026-07-26
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)
Goal: a tracked suite_tcl86 buildsuite builds a Tcl 8.6 windows runtime from fossil core-8-6-branch sources using 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 - an 8.6-compatible thread branch and tclvfs - and gates the result against the 8.6 core testsuite via parsed all.tcl totals against a tracked, dispositioned expected-failure baseline, mirroring the suite_tcl90 gate mechanism.
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.
@ -86,13 +86,73 @@ later.
the zig-only recipe compiles compat/zlib statically instead (decision
recorded). TCLOBJS composition, in-tree libtommath, TCL_BROKEN_MAINARGS
in win/tclAppInit.c, dde 1.4.6 / registry 1.3.5 versions recorded.
- Remains for acceptance: author build86.zig (COREOBJS/PLATFORMOBJS + defines,
static zlib, tommath, manifest/rc step, entry-point handling), thread 2.8 +
tclvfs build wiring, install-tree layout + hermetic resolution proof,
suite.tcl test -> test-gate with parsed totals, FIRST CORE-TEST CENSUS and
the dispositioned expected_test_failures.txt baseline (file deliberately not
created until the census exists), two-consecutive-run deterministic PASS
record, interactive-smoke record.
- 2026-07-26 completion increment (acceptance met; all "Remains for
acceptance" items resolved):
- RECIPE AUTHORED (build86.zig): COREOBJS/PLATFORMOBJS/TOMMATHOBJS source
lists + the rules.vc-derived flag classes (app/pkg/stub with static
variants), static zlib compiled into both the dll and the static shell
(build_zlib86 - the 11-file makefile.vc ZLIBOBJS static-build list;
stock nmake links a prebuilt zdll.lib for shared builds, so no zlib1.dll
ships), in-tree libtommath, tclUuid.h + tclsh.exe.manifest generated into
the build cache and delivered via overlay include dirs / rc copies.
- TWO BUILD-SURFACED FINDINGS (both recorded in suite_tcl86/README.md
DERIVATION): (a) makefile.vc PRJ_DEFINES MP_FIXED_CUTOFFS is load-bearing
- TOMMATHOBJS carries no bn_cutoffs.c, so the karatsuba/toom cutoff
variables are undefined at link without it; (b) TCL_BROKEN_MAINARGS
(win/tcl.m4 gcc-class extra_cflags) + the console subsystem give
tclAppInit.c a plain main() + GetCommandLineW re-parse - without it mingw's
fallback demands WinMain.
- PRODUCTS (stock threaded-release naming - rules.vc keeps SUFX 't' on 8.6):
bin/tcl86t.dll + tcl86t.lib import lib, bin/tclsh86ts.exe (static,
kit-class: dde/registry compiled in via TCL_USE_STATIC_PACKAGES),
bin/tclsh86t.exe (dynamic pair against the dll), lib/libtclstub86.a.
- COMPANIONS: thread 2.8.13 (lib/thread2.8.13/thread2813t.dll, TEA installed
shape from the checkout's own pkgIndex.tcl.in + ttrace.tcl) and tclvfs
1.4.2 (lib/vfs1.4.2/vfs142t.dll + configured vfs.tcl/pkgIndex.tcl; the
vfs<nodots>t TEA dll name lets Tcl's load-time name guessing derive
Vfs_Init) build and load; registry 1.3.5 / dde 1.4.6 in lib/tcl8.6/{reg,dde}.
- INSTALL TREE + HERMETIC RESOLUTION PROVEN: whole library tree ->
lib/tcl8.6, tm modules -> lib/tcl8/{8.5,8.6}/ (versions parsed from the
library pkgIndex files: http 2.9.9, msgcat 1.6.1, tcltest 2.5.11,
platform 1.1.1, platform::shell 1.1.4). Both shells report
[info library] = the installed lib/tcl8.6 with TCLLIBPATH/TCL_LIBRARY
unset (scrubTclEnv per built-shell run + suite.tcl process-level unset) -
exe/dll-relative discovery, no external Tcl.
- CORE-TEST GATE: suite.tcl test -> zig build test-gate runs the 8.6 core
testsuite under the installed static shell, parses all.tcl totals (exit
codes untrusted, tools/test_gate.tcl), gates vs the tracked dispositioned
baseline. Per-library gates (test-thread, test-tclvfs) added on the
suite_tcl90 G-107 policy-tier pattern.
- FIRST CENSUS + baselines (2026-07-26, tclsh86ts.exe 8.6.18, machine
SuperBee): core 46525 run / 2 failed (filename-16.12/16.13 - windows
admin-share self-globbing machine trait, same class as suite_tcl90's
baseline; the 9.0.5 fCmd 'file home' failures are absent - 'file home' is a
8.7/9.0 command); thread 142 run / 0 failed (EMPTY baseline; constraint-
driven skips only); tclvfs 51 run / 2 failed (vfsUrl-1.2/2.1 - external FTP
dependency on the retired ftp://ftp.tcl.tk, the same 1.4.2-era entries
suite_tcl90's tclvfs note records). Baselines authored with every entry
dispositioned: expected_test_failures.txt / _thread.txt / _tclvfs.txt.
- TWO-CONSECUTIVE-RUN DETERMINISTIC PASS (2026-07-26): both `suite.tcl test
-steps "test-gate test-libraries"` runs -> GATE PASS on all three suites,
identical dispositioned failure sets (core 2, thread 0, tclvfs 2). One core
test jittered pass<->skip between runs (39227/7296 vs 39226/7297; a
constraint-evaluation timing effect) but the gate criterion - failed names
vs baseline - was identical, so the gate is deterministic.
- INTERACTIVE-SMOKE RECORD (2026-07-26, both shells, env scrubbed): script
execution (expr 6*7 = 42), [info patchlevel] = 8.6.18, [info library] =
installed lib/tcl8.6, and package require Thread 2.8.13 / vfs 1.4.2 /
registry 1.3.5 / dde 1.4.6 / http 2.9.9 / platform 1.1.1 all load; the
suite's `smoke` step (tools/suite_smoke.tcl + pkg_smoke.tcl) captures the
same on every build.
- FIREWALL NOTE (unattended-run trap, characterized): socket.test's
unconditional constraint-probe `socket -server {} 0` (no -myaddr -> wildcard
0.0.0.0 bind) triggers the one-time Windows Defender "allow app" dialog on a
freshly-built exe's first listen. Once accepted for the stable installed exe
path (out/bin/tclsh86ts.exe) the rule persists across re-runs and rebuilds,
so runs 2+ are unattended (the two-run determinism proof needed no
interaction). The remote-server socket tests themselves skip under the
serverNeeded constraint (1996 skips). This is the buildsuites-direction
memory's recorded "firewall dialog on new exe's first socket listen" trap.
## Notes

8
src/buildsuites/AGENTS.md

@ -40,6 +40,8 @@ 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 derivation 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 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)

60
src/buildsuites/suite_tcl86/README.md

@ -8,23 +8,36 @@ 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 derivation IN PROGRESS. `suite.tcl build` stages the source
checkouts (useful on its own - the derivation studies the staged
`tcl86/win/makefile.vc`) and then fails at the `build86.zig` skeleton with a
clear pending message. Progress ledger: `goals/G-099-suite-tcl86-buildsuite.md`.
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`.
## Deliverables (per the G-099 acceptance)
## Deliverables (per the G-099 acceptance; stock threaded-release naming)
- `tclsh86.exe` + tcl86 dll + installed lib tree the shell resolves without
external Tcl installations (TCLLIBPATH/TCL_LIBRARY unset in all suite child
invocations). NO zipfs in 8.6 - no self-contained szip runtime is possible;
this suite delivers exe + dll + on-disk lib tree only (kit containment is
G-101 territory).
- thread (thread-2-8-branch) and tclvfs (trunk) built and loading in that shell.
- `bin/tclsh86ts.exe` (static shell, kit-class - dde/registry compiled in via
TCL_USE_STATIC_PACKAGES), `bin/tclsh86t.exe` + `bin/tcl86t.dll` (stock
dynamic pair; `lib/tcl86t.lib` import lib, `lib/libtclstub86.a` stub lib)
with the installed `lib/tcl8.6` tree both shells resolve hermetically
(TCLLIBPATH/TCL_LIBRARY unset in all suite child invocations; resolution is
exe/dll-relative). NO zipfs in 8.6 - no self-contained szip runtime is
possible; this suite delivers exe + dll + on-disk lib tree only (kit
containment is G-101 territory).
- thread 2.8.13 (`lib/thread2.8.13/thread2813t.dll`, TEA installed shape with
the checkout's own pkgIndex template + ttrace.tcl) and tclvfs 1.4.2
(`lib/vfs1.4.2/vfs142t.dll` + configured vfs.tcl/pkgIndex.tcl + script
library) - `package require Thread/vfs` proven in both shells, plus
registry 1.3.5 / dde 1.4.6 (`lib/tcl8.6/{reg,dde}/`).
- tm modules under `lib/tcl8/{8.5,8.6}/` with versions parsed from the
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 pending (the 9.0.5 census does not transfer; machine
traits dispositioned there will likely reappear under 8.6 test names).
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`).
## Sources (both flows carry the same identity)
@ -82,13 +95,22 @@ Context, verified 2026-07-25):
versions derive from `library/init.tcl` + `generic/tcl.h`
(TCL_PATCH_LEVEL "8.6.18") - version-derived output names (tcl86t.dll
naming class, tclsh86 vs tclsh8.6 spellings) settle with the recipe.
- makefile.vc `PRJ_DEFINES` (line 427) rides EVERY flag class via rules.vc:
`MP_PREC=4` + `MP_FIXED_CUTOFFS` are load-bearing (TOMMATHOBJS carries no
bn_cutoffs.c - without fixed cutoffs the cutoff variables are undefined at
link). The MSVC-isms there (`inline=__inline`, `_CRT_*_NO_DEPRECATE`,
`z_off_t=__int64`) are deliberately not translated for clang/mingw.
Remaining derivation work (tracked in G-099 Progress): express COREOBJS/
PLATFORMOBJS source lists + defines in build86.zig (build905.zig +
build_common.zig as the shape reference), decide static-vs-dll core linkage
for the punk86-class consumers, port the manifest/rc resource step, wire
thread 2.8 + tclvfs builds (TEA-style configure products generated
recipe-side as in 9.0), then the core-test gate + first census.
Derivation COMPLETE (2026-07-26): all the above are expressed in build86.zig -
COREOBJS/PLATFORMOBJS/TOMMATHOBJS source lists + the rules.vc-derived flag
classes (app/pkg/stub, static variants), static zlib compiled into both the
dll and the static shell, tclUuid.h + tclsh.exe.manifest generated into the
build cache and delivered via overlay include dirs / rc copies, entry points
via TCL_BROKEN_MAINARGS + console subsystem, thread 2.8 + tclvfs built and
installed, the whole-tree library install + tm modules, and the core/thread/
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.
## Flows

7
src/buildsuites/suite_tcl86/build.zig

@ -0,0 +1,7 @@
//build.zig - default-entry shim so plain 'zig build <step>' works from this
//directory (the bootstrap-flow invocations in the README). The recipe itself
//lives in build86.zig - a deliberately VERSIONED name (the staged flows invoke
//it explicitly via --build-file, and a future suite may carry more than one
//recipe file). build.zig.zon beside this file serves both entry points.
const recipe = @import("build86.zig");
pub const build = recipe.build;

915
src/buildsuites/suite_tcl86/build86.zig

@ -1,20 +1,905 @@
//build86.zig - suite_tcl86 zig recipe (G-099) - DERIVATION IN PROGRESS.
//build86.zig - suite_tcl86 zig recipe (G-099): Tcl core-8-6-branch windows
//runtime from the pinned zig toolchain only, plus the core binary companions
//punkshell's Tcl 8 targets need (thread 2.8, tclvfs trunk), gated against the
//8.6 core testsuite.
//
//This is a deliberate pending-state skeleton: 'suite.tcl build' stages the
//core-8-6-branch / thread-2-8-branch / tclvfs-trunk sources (useful on its own -
//the checkout carries win/makefile.vc, the derivation source of truth) and then
//fails HERE with a clear message instead of pretending to build.
//Forked from suite_tcl90/build905.zig (the shape reference; duplication-with-a-
//note is the recorded posture) and DERIVED from the staged 8.6.18
//win/makefile.vc + rules.vc - the object inventory, per-object flag classes
//(appcflags/pkgcflags/stubscflags), naming (SUFX rules) and install layout all
//mirror that authority. The full derivation record lives in README.md
//DERIVATION; key differences from the 9.0.5 recipe:
// - NO zipfs in 8.6: no szip self-contained runtime, no zip staging steps.
// The runtime is exe + dll + on-disk lib tree; hermetic resolution is
// exe/dll-relative (../lib/tcl8.6 from bin/, via tclWinInit.c).
// - zlib: 8.6's makefile.vc links a PREBUILT zdll.lib for shared builds and
// compiles the 11-file ZLIBOBJS list only for static ones. This recipe
// compiles that list into a static lib linked into BOTH the dll and the
// static shell - no zlib1.dll is shipped (deviation recorded in README).
// - entry points: gcc-class 8.6 builds define TCL_BROKEN_MAINARGS (win/
// tcl.m4:671 extra_cflags - carried globally here for parity): the CRT
// entry is plain main() (console subsystem) and the real command line is
// re-parsed from GetCommandLineW by tclAppInit.c's setargv. Without the
// define tclAppInit provides only _tmain/wmain and mingw's fallback main
// demands WinMain. No -municode / mingw_unicode_entry_point (unlike the
// 9.0.5 recipe, whose sources dropped TCL_BROKEN_MAINARGS).
// - tclMain.c is compiled twice BY UPSTREAM DESIGN (COREOBJS carries both
// tclMain.obj and tclMainW.obj): the UNICODE copy provides Tcl_MainExW,
// the plain copy Tcl_MainEx + the once-only support functions (8.6 guards
// them with #ifndef UNICODE - no TCL_ASCII_MAIN needed).
// - stub lib is 3 objects (tclStubLib, tclTomMathStubLib, tclOOStubLib - no
// tclStubCall/tclStubLibTbl/tclWinPanic in 8.6); named libtclstub86.
// - dde/registry: stubs-consumer dlls tcldde14.dll (1.4) / tclreg13.dll
// (1.3) installed into the script library's dde/ and reg/ package dirs;
// the STATIC shell instead compiles tclWinDde/tclWinReg in and registers
// them via TCL_USE_STATIC_PACKAGES (makefile.vc static-build parity).
// - tm modules install under lib/tcl8/{8.5,8.6}/ with versions parsed from
// the library pkgIndex files (makefile.vc nmakehlp -V parity).
// - MP_64BIT / MP_PREC / MP_FIXED_CUTOFFS are NOT defined (8.6's makefile.vc
// defines none of them; the in-tree libtommath self-configures - all TUs
// share one flag set so the mp_digit ABI stays consistent).
//
//Derivation record (what this recipe must express, from 8.6's win/makefile.vc):
//see README.md section DERIVATION. The 9.0.5 recipe (suite_tcl90/build905.zig +
//build_common.zig + build_zlib/build_libtommath/build_tclvfs/build_tclthread
//helper modules) is the shape reference; the G-099 Context records the known
//8.6 differences to verify rather than assume (bundled zlib/libtommath
//arrangements, no tclUuid.h, makefile.vc-derived manifest/rc generation,
//TCL_BROKEN_MAINARGS-era entry points, no zipfs).
//Products (stock threaded-release naming - rules.vc keeps the 't' SUFX on 8.6):
// bin/tcl86t.dll + tcl86t.lib shared core (+import lib for extensions)
// bin/tclsh86t.exe dynamic shell linked against the dll
// bin/tclsh86ts.exe static shell (kit-class; dde/registry static)
// lib/libtclstub86.a stub lib for stubs-consumer extension builds
// lib/tcl8.6/... script library tree (dde/reg dlls inside)
// lib/tcl8/8.x/... tm modules; lib/thread2.8.13, lib/vfs1.4.2
//
//Windows-only for the G-099 arc: cross-target parameterization is G-105's -
//the single-target shape here deliberately avoids per-target tree copies so a
//target dimension can be added without restructuring.
const std = @import("std");
const builtin = @import("builtin");
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 tcl_dot_version = "8.6";
const tcl_nodot_version = "86";
const tcl_source_folder = "../tcl86";
//stock makefile.vc/rules.vc naming for a threaded release build (SUFX 't'
//kept while TCL_THREADS && TCL_VERSION <= 86; static adds 's')
const tcl_dll_name = "tcl" ++ tcl_nodot_version ++ "t";
const tcl_dll_file = tcl_dll_name ++ ".dll";
const tclsh_dynamic = "tclsh" ++ tcl_nodot_version ++ "t";
const tclsh_static = "tclsh" ++ tcl_nodot_version ++ "ts";
const tcl_stub_lib_file = "libtclstub" ++ tcl_nodot_version ++ ".a";
const dde_dll_file = "tcldde14.dll"; //DDEDOTVERSION 1.4, SUFX 't' stripped for package dlls
const reg_dll_file = "tclreg13.dll"; //REGDOTVERSION 1.3
comptime {
//The suite pin (G-102 convention: the recipe records the zig version it is
//written against). The recipe is 0.16-API; punk-getzig materializes the
//pinned toolchain (see README Pinned zig).
const required_zig = "0.16.0";
const current_zig = builtin.zig_version;
const min_zig = std.SemanticVersion.parse(required_zig) catch unreachable;
if (current_zig.order(min_zig) == .lt) {
const error_message =
\\This recipe is written against zig {} (see README Pinned zig) and
\\does not build with older zigs. bin/punk-getzig fetches the pinned
\\toolchain; https://ziglang.org/download/ hosts releases.
\\
;
@compileError(std.fmt.comptimePrint(error_message, .{min_zig}));
}
}
pub fn build(b: *std.Build) !void {
//G-102 invocation modes (suite_tcl90 parity). STAGED mode: build root is the
//staged recipe copy (_build/<suite>/build) with source trees beside it
//(../tcl86 etc). BOOTSTRAP mode: build root is the TRACKED suite dir - only
//staging steps register ('zig build stage|bootstrap', the no-tclsh entry).
if (!common.pathExists(b, tcl_source_folder)) {
if (common.pathExists(b, "sources.config")) {
try bootstrapMode(b);
return;
}
@panic("suite recipe: no source trees found beside the recipe (" ++ tcl_source_folder ++
" missing). Use 'zig build stage' (or bootstrap) from the tracked suite dir for the " ++
"pinned zon flow, or 'tclsh suite.tcl build' for the fossil dev flow.");
}
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
if (target.result.os.tag != .windows) {
@panic("suite_tcl86 recipe is windows-only for the G-099 arc (a Tcl 8.6 WINDOWS runtime); cross-target parameterization is G-105's goal");
}
const prefix = try std.fmt.allocPrint(b.allocator, "{s}", .{b.install_path});
const bindir = b.pathJoin(&.{ prefix, "bin" });
const libdir = b.pathJoin(&.{ prefix, "lib" });
const includedir = b.pathJoin(&.{ prefix, "include" });
const mandir = b.pathJoin(&.{ prefix, "man" });
const tcl_library = b.pathJoin(&.{ prefix, "lib", "tcl" ++ tcl_dot_version });
const script_install_dir_rel = "lib/tcl" ++ tcl_dot_version;
const module_install_dir_rel = "lib/tcl8"; //tm root: tm.tcl scans lib/tcl8/8.4..8.6
std.debug.print("prefix {s}\n", .{prefix});
std.debug.print("tcl_library {s}\n", .{tcl_library});
//-- version facts from the staged tree (never hardcoded)
const tclh_content = common.readSourceFile(b, tcl_source_folder ++ "/generic/tcl.h");
const tcl_h_version = common.parseDefineString(tclh_content, "TCL_VERSION") orelse @panic("TCL_VERSION not found in tcl.h");
const tcl_h_patchlevel = common.parseDefineString(tclh_content, "TCL_PATCH_LEVEL") orelse @panic("TCL_PATCH_LEVEL not found in tcl.h");
if (!std.mem.eql(u8, tcl_h_version, tcl_dot_version)) {
std.debug.panic("staged tree is Tcl {s} but this recipe expresses Tcl " ++ tcl_dot_version ++ " (sources.config tcl ref / dir mismatch?)", .{tcl_h_version});
}
const tcl_win_version = common.winResourceVersion(b, tcl_h_version, tcl_h_patchlevel);
std.debug.print("tcl.h: TCL_PATCH_LEVEL {s} (TCL_WIN_VERSION {s})\n", .{ tcl_h_patchlevel, tcl_win_version });
//-- static zlib (see header: compiled into dll AND static shell)
const zlib_lib_compile = try build_zlib86(tcl_source_folder ++ "/compat/zlib", b, target, optimize);
//-- generated configure-products into the build cache (G-102 convention:
//source trees are never written; delivery via overlay include dirs / overlay
//rc copies).
//tclUuid.h: 8.6.18's makefile.vc concatenates win/tclUuid.h.in + the
//checkout's manifest.uuid - wrapfiletofile reproduces exactly that (the .in
//is the one '#define TCL_VERSION_UUID \' line). tclEvent.c #includes it.
const wrap_exe = b.addExecutable(.{
.name = "wrapfiletofile",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/wrapfiletofile.zig"),
.target = b.graph.host,
}),
});
const wrap_run = b.addRunArtifact(wrap_exe);
wrap_run.addArgs(&.{
"-prefix",
"#define TCL_VERSION_UUID \\",
"-prefixnl",
"1",
"-input",
});
wrap_run.addFileArg(b.path(tcl_source_folder ++ "/manifest.uuid"));
wrap_run.addArg("-output");
const tcluuid_file1 = wrap_run.addOutputFileArg("tclUuid.h");
//include-dir overlay FIRST at each consumer so a stale tree-written copy can
//never shadow the generated one; also carries the generation-step dependency.
const tcluuid_include_dir = tcluuid_file1.dirname();
//tclsh.exe.manifest (tclsh.rc references it rc-file-relative, so the rc is
//compiled from an overlay COPY beside the generated manifest).
const tclsh_manifest_content = common.replaceAll(b, common.replaceAll(b, common.readSourceFile(b, tcl_source_folder ++ "/win/tclsh.exe.manifest.in"), "@TCL_WIN_VERSION@", tcl_win_version), "@MACHINE@", "AMD64");
const wf_tclshrc = b.addWriteFiles();
_ = wf_tclshrc.add("tclsh.exe.manifest", tclsh_manifest_content);
const tclshrc_overlay = wf_tclshrc.addCopyFile(b.path(tcl_source_folder ++ "/win/tclsh.rc"), "tclsh.rc");
//-- flag classes, mirroring rules.vc composition for a threaded release
//build. OPTDEFINES equivalents translated for zig cc (clang/mingw-class):
//HAVE_NO_SEH (no __try), HAVE_WSPIAPI_H (guard exists in tclWinPort.h),
//header HAVEs clang satisfies; NO ZIPFS/MP defines (see header).
var ac_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer ac_flags.deinit();
try ac_flags.appendSlice(&.{
"-pipe",
"-finput-charset=UTF-8",
"-DPACKAGE_NAME=\"tcl\"",
"-DPACKAGE_TARNAME=\"tcl\"",
"-DPACKAGE_VERSION=\"" ++ tcl_dot_version ++ "\"",
"-DPACKAGE_STRING=\"tcl " ++ tcl_dot_version ++ "\"",
"-DPACKAGE_BUGREPORT=\"\"",
"-DPACKAGE_URL=\"\"",
"-DSTDC_HEADERS=1",
"-DHAVE_STDINT_H=1",
"-DHAVE_INTTYPES_H=1",
"-DHAVE_STDBOOL_H=1",
"-DHAVE_STDIO_H=1",
//8.6-era threading defines (rules.vc: TCL_THREADS always 1 on windows,
//USE_THREAD_ALLOC rides it while TCL_VERSION < 87)
"-DTCL_THREADS=1",
"-DUSE_THREAD_ALLOC=1",
"-DNDEBUG=1",
"-DTCL_CFG_OPTIMIZED=1",
"-DTCL_CFG_DO64BIT=1",
"-DHAVE_NO_SEH=1",
"-DHAVE_WSPIAPI_H=1",
"-DHAVE_ZLIB=1",
"-DMODULE_SCOPE=extern",
//makefile.vc PRJ_DEFINES (rides every flag class via rules.vc): the
//in-core libtommath uses FIXED cutoffs - TOMMATHOBJS carries no
//bn_cutoffs.c, so without MP_FIXED_CUTOFFS the cutoff variables are
//undefined at link. (inline=__inline, _CRT_*_NO_DEPRECATE and
//z_off_t=__int64 there are MSVC-isms not translated for clang/mingw.)
"-DMP_PREC=4",
"-DMP_FIXED_CUTOFFS",
//gcc-class entry handling (win/tcl.m4:671 extra_cflags): tclAppInit.c
//provides plain main() + setargv re-parse only under this define - see
//the entry-points note in the file header
"-DTCL_BROKEN_MAINARGS",
});
var config_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer config_flags.deinit();
try config_flags.appendSlice(&.{
try std.fmt.allocPrint(b.allocator, "-DCFG_RUNTIME_LIBDIR=\"{s}\"", .{libdir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_RUNTIME_BINDIR=\"{s}\"", .{bindir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_RUNTIME_INCDIR=\"{s}\"", .{includedir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_RUNTIME_SCRDIR=\"{s}\"", .{tcl_library}),
try std.fmt.allocPrint(b.allocator, "-DCFG_RUNTIME_DOCDIR=\"{s}\"", .{mandir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_INSTALL_LIBDIR=\"{s}\"", .{libdir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_INSTALL_BINDIR=\"{s}\"", .{bindir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_INSTALL_INCDIR=\"{s}\"", .{includedir}),
try std.fmt.allocPrint(b.allocator, "-DCFG_INSTALL_SCRDIR=\"{s}\"", .{tcl_library}),
try std.fmt.allocPrint(b.allocator, "-DCFG_INSTALL_DOCDIR=\"{s}\"", .{mandir}),
//consumed by tclPkgConfig.c only when !STATIC_BUILD - safe globally
"-DCFG_RUNTIME_DLLFILE=\"" ++ tcl_dll_file ++ "\"",
});
var warning_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer warning_flags.deinit();
try warning_flags.appendSlice(&.{
"-Wall",
"-Wextra",
"-Wshadow",
"-Wundef",
"-Wwrite-strings",
"-Wpointer-arith",
"-fextended-identifiers",
});
var c_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer c_flags.deinit();
try c_flags.appendSlice(&.{
"-O3",
"-fomit-frame-pointer",
"-finput-charset=UTF-8",
//required on windows (suite_tcl90 finding: 'linsert {} 0 a a' class
//crashes without both) - clang UBSan traps must stay fully off
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
//app-class flags (rules.vc appcflags: everything EXCEPT BUILD_tcl)
var app_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer app_flags.deinit();
try app_flags.appendSlice(c_flags.items);
try app_flags.appendSlice(warning_flags.items);
try app_flags.appendSlice(config_flags.items);
try app_flags.appendSlice(ac_flags.items);
//pkg-class flags (rules.vc pkgcflags: + BUILD_tcl) - core objects, tommath
var pkg_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer pkg_flags.deinit();
try pkg_flags.appendSlice(app_flags.items);
try pkg_flags.appendSlice(&.{"-DBUILD_tcl"});
var pkg_static_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer pkg_static_flags.deinit();
try pkg_static_flags.appendSlice(pkg_flags.items);
try pkg_static_flags.appendSlice(&.{"-DSTATIC_BUILD"});
//stubs-class flags (rules.vc stubscflags: STATIC_BUILD always, no BUILD_tcl,
//no USE_TCL_STUBS while DOING_TCL)
var stub_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer stub_flags.deinit();
try stub_flags.appendSlice(app_flags.items);
try stub_flags.appendSlice(&.{ "-DSTATIC_BUILD", "-fno-lto" });
//tclMainW copies (makefile.vc: tclMain.c + /DUNICODE /D_UNICODE)
var mainw_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer mainw_flags.deinit();
try mainw_flags.appendSlice(pkg_flags.items);
try mainw_flags.appendSlice(&.{ "-DUNICODE", "-D_UNICODE" });
var mainw_static_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer mainw_static_flags.deinit();
try mainw_static_flags.appendSlice(pkg_static_flags.items);
try mainw_static_flags.appendSlice(&.{ "-DUNICODE", "-D_UNICODE" });
//tclAppInit copies (makefile.vc: appcflags + UNICODE + TCL_USE_STATIC_PACKAGES)
var appinit_dyn_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer appinit_dyn_flags.deinit();
try appinit_dyn_flags.appendSlice(app_flags.items);
try appinit_dyn_flags.appendSlice(&.{ "-DUNICODE", "-D_UNICODE", "-DTCL_USE_STATIC_PACKAGES=0" });
var appinit_static_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer appinit_static_flags.deinit();
try appinit_static_flags.appendSlice(app_flags.items);
try appinit_static_flags.appendSlice(&.{ "-DSTATIC_BUILD", "-DUNICODE", "-D_UNICODE", "-DTCL_USE_STATIC_PACKAGES=1" });
//stubs-consumer extension objects (makefile.vc: appcflags + USE_TCL_STUBS;
//static-shell copies add STATIC_BUILD per OPTDEFINES)
var stubext_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer stubext_flags.deinit();
try stubext_flags.appendSlice(app_flags.items);
try stubext_flags.appendSlice(&.{"-DUSE_TCL_STUBS=1"});
var stubext_static_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer stubext_static_flags.deinit();
try stubext_static_flags.appendSlice(app_flags.items);
try stubext_static_flags.appendSlice(&.{ "-DSTATIC_BUILD", "-DUSE_TCL_STUBS=1" });
//-- source inventory (win/makefile.vc COREOBJS/PLATFORMOBJS/TOMMATHOBJS,
//8.6.18 staged tree). tclMain.c appears ONCE here - the per-consumer loops
//compile it with mainw flags (the tclMainW.obj copy) and each consumer adds
//the plain ansi copy explicitly (both objs are in stock COREOBJS).
var generic_sources = std.array_list.Managed([]const u8).init(b.allocator);
defer generic_sources.deinit();
const generic_names = [_][]const u8{
"regcomp", "regerror", "regexec", "regfree",
"tclAlloc", "tclAssembly", "tclAsync", "tclBasic",
"tclBinary", "tclCkalloc", "tclClock", "tclCmdAH",
"tclCmdIL", "tclCmdMZ", "tclCompCmds", "tclCompCmdsGR",
"tclCompCmdsSZ", "tclCompExpr", "tclCompile", "tclConfig",
"tclDate", "tclDictObj", "tclDisassemble", "tclEncoding",
"tclEnsemble", "tclEnv", "tclEvent", "tclExecute",
"tclFCmd", "tclFileName", "tclGet", "tclHash",
"tclHistory", "tclIndexObj", "tclInterp", "tclIO",
"tclIOCmd", "tclIOGT", "tclIOSock", "tclIOUtil",
"tclIORChan", "tclIORTrans", "tclLink", "tclListObj",
"tclLiteral", "tclLoad", "tclMain", "tclNamesp",
"tclNotify", "tclOO", "tclOOBasic", "tclOOCall",
"tclOODefineCmds", "tclOOInfo", "tclOOMethod", "tclOOStubInit",
"tclObj", "tclOptimize", "tclPanic", "tclParse",
"tclPathObj", "tclPipe", "tclPkg", "tclPkgConfig",
"tclPosixStr", "tclPreserve", "tclProc", "tclRegexp",
"tclResolve", "tclResult", "tclScan", "tclStringObj",
"tclStrToD", "tclStubInit", "tclThread", "tclThreadAlloc",
"tclThreadJoin", "tclThreadStorage", "tclTimer", "tclTomMathInterface",
"tclTrace", "tclUtf", "tclUtil", "tclVar",
"tclZlib",
};
inline for (generic_names) |nm| {
try generic_sources.append(tcl_source_folder ++ "/generic/" ++ nm ++ ".c");
}
var win_sources = std.array_list.Managed([]const u8).init(b.allocator);
defer win_sources.deinit();
const win_names = [_][]const u8{
"tclWin32Dll", "tclWinChan", "tclWinConsole", "tclWinError",
"tclWinFCmd", "tclWinFile", "tclWinInit", "tclWinLoad",
"tclWinNotify", "tclWinPipe", "tclWinSerial", "tclWinSock",
"tclWinThrd", "tclWinTime",
};
inline for (win_names) |nm| {
try win_sources.append(tcl_source_folder ++ "/win/" ++ nm ++ ".c");
}
var tommath_sources = std.array_list.Managed([]const u8).init(b.allocator);
defer tommath_sources.deinit();
const tommath_names = [_][]const u8{
"bn_mp_add", "bn_mp_add_d", "bn_mp_and", "bn_mp_clamp",
"bn_mp_clear", "bn_mp_clear_multi", "bn_mp_cmp", "bn_mp_cmp_d",
"bn_mp_cmp_mag", "bn_mp_cnt_lsb", "bn_mp_copy", "bn_mp_count_bits",
"bn_mp_div", "bn_mp_div_d", "bn_mp_div_2", "bn_mp_div_2d",
"bn_s_mp_div_3", "bn_mp_exch", "bn_mp_expt_n", "bn_mp_grow",
"bn_mp_init", "bn_mp_init_copy", "bn_mp_init_multi", "bn_mp_init_set",
"bn_mp_init_size", "bn_mp_lshd", "bn_mp_mod", "bn_mp_mod_2d",
"bn_mp_mul", "bn_mp_mul_2", "bn_mp_mul_2d", "bn_mp_mul_d",
"bn_mp_neg", "bn_mp_or", "bn_mp_pack", "bn_mp_pack_count",
"bn_mp_radix_size", "bn_mp_radix_smap", "bn_mp_read_radix", "bn_mp_rshd",
"bn_mp_set", "bn_mp_shrink", "bn_mp_sqr", "bn_mp_sqrt",
"bn_mp_sub", "bn_mp_sub_d", "bn_mp_signed_rsh", "bn_mp_to_ubin",
"bn_mp_to_radix", "bn_mp_ubin_size", "bn_mp_unpack", "bn_mp_xor",
"bn_mp_zero", "bn_s_mp_add", "bn_s_mp_balance_mul", "bn_s_mp_karatsuba_mul",
"bn_s_mp_karatsuba_sqr", "bn_s_mp_mul_digs", "bn_s_mp_mul_digs_fast", "bn_s_mp_reverse",
"bn_s_mp_sqr", "bn_s_mp_sqr_fast", "bn_s_mp_sub", "bn_s_mp_toom_sqr",
"bn_s_mp_toom_mul",
};
inline for (tommath_names) |nm| {
try tommath_sources.append(tcl_source_folder ++ "/libtommath/" ++ nm ++ ".c");
}
var core_sources = std.array_list.Managed([]const u8).init(b.allocator);
defer core_sources.deinit();
try core_sources.appendSlice(generic_sources.items);
try core_sources.appendSlice(win_sources.items);
try core_sources.appendSlice(tommath_sources.items);
const core_include = struct {
fn apply(m: *std.Build.Module, bb: *std.Build, uuid_dir: std.Build.LazyPath) void {
m.addIncludePath(uuid_dir); //overlay first (G-102: stale tree copies can never shadow)
m.addIncludePath(bb.path(tcl_source_folder ++ "/generic"));
m.addIncludePath(bb.path(tcl_source_folder ++ "/win"));
m.addIncludePath(bb.path(tcl_source_folder ++ "/libtommath"));
m.addIncludePath(bb.path(tcl_source_folder ++ "/compat/zlib"));
}
}.apply;
const win_syslibs = struct {
fn apply(m: *std.Build.Module) void {
//rules.vc baselibs
m.linkSystemLibrary("netapi32", .{});
m.linkSystemLibrary("kernel32", .{});
m.linkSystemLibrary("user32", .{});
m.linkSystemLibrary("advapi32", .{});
m.linkSystemLibrary("userenv", .{});
m.linkSystemLibrary("ws2_32", .{});
}
}.apply;
// ================================================================
//stub lib: libtclstub86 (TCLSTUBOBJS = tclStubLib, tclTomMathStubLib,
//tclOOStubLib - the 8.6 set)
const finalstublib = b.addLibrary(.{
.linkage = .static,
.name = "libtclstub" ++ tcl_nodot_version,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
core_include(finalstublib.root_module, b, tcluuid_include_dir);
inline for (.{ "tclStubLib", "tclTomMathStubLib", "tclOOStubLib" }) |nm| {
finalstublib.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/generic/" ++ nm ++ ".c"),
.flags = stub_flags.items,
});
}
finalstublib.root_module.link_libc = true;
//ar'd copy under the mingw-conventional archive name -> lib/libtclstub86.a
//(the extension-builder consumable; the s flag is ranlib-equivalent)
const wf_stub = b.addWriteFiles();
const lz_stublib_file = wf_stub.addCopyFile(finalstublib.getEmittedBin(), b.fmt("buildarchive/{s}", .{finalstublib.out_filename}));
const create_stublib_archive = b.addSystemCommand(&.{ b.graph.zig_exe, "ar", "crs", "--" });
const stublib_archive_file = create_stublib_archive.addOutputFileArg("buildarchive/" ++ tcl_stub_lib_file);
create_stublib_archive.addFileArg(lz_stublib_file);
const installed_stublib_archive = b.addInstallFileWithDir(stublib_archive_file, .prefix, "lib/" ++ tcl_stub_lib_file);
installed_stublib_archive.step.dependOn(&create_stublib_archive.step);
b.getInstallStep().dependOn(&installed_stublib_archive.step);
// ================================================================
//tcl86t.dll - the shared core (zlib compiled in statically; see header)
const tcl_lib_shared = b.addLibrary(.{
.linkage = .dynamic,
.name = tcl_dll_name,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
core_include(tcl_lib_shared.root_module, b, tcluuid_include_dir);
for (core_sources.items) |src| {
const flags = if (std.mem.eql(u8, std.fs.path.stem(src), "tclMain")) mainw_flags.items else pkg_flags.items;
tcl_lib_shared.root_module.addCSourceFile(.{ .file = b.path(src), .flags = flags });
}
//the plain (ansi) tclMain copy - Tcl_MainEx + the #ifndef UNICODE support fns
tcl_lib_shared.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/generic/tclMain.c"),
.flags = pkg_flags.items,
});
tcl_lib_shared.root_module.linkLibrary(zlib_lib_compile);
win_syslibs(tcl_lib_shared.root_module);
tcl_lib_shared.root_module.link_libc = true;
tcl_lib_shared.root_module.addWin32ResourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tcl.rc"),
.include_paths = &.{
b.path(tcl_source_folder ++ "/generic"),
b.path(tcl_source_folder ++ "/win"),
},
});
b.installArtifact(tcl_lib_shared); //bin/tcl86t.dll (+ implib -> lib/)
// ================================================================
//tclsh86ts.exe - the static shell (kit-class). STATIC_BUILD everywhere,
//tclWinReg/tclWinDde compiled in and registered via TCL_USE_STATIC_PACKAGES
//(makefile.vc static-build PLATFORMOBJS parity), stub objs linked (the nmake
//TCLSH link includes TCLSTUBLIB), entry = plain main (TCL_BROKEN_MAINARGS).
const tclsh_static_exe = b.addExecutable(.{
.name = tclsh_static,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
tclsh_static_exe.stack_size = 2300000; //makefile.vc CONEXECMD -stack:2300000
//console subsystem, C-provided main(): without this zig infers a windows-
//subsystem entry for the zig-root exe and the link demands WinMain
tclsh_static_exe.subsystem = .Console;
core_include(tclsh_static_exe.root_module, b, tcluuid_include_dir);
for (core_sources.items) |src| {
const flags = if (std.mem.eql(u8, std.fs.path.stem(src), "tclMain")) mainw_static_flags.items else pkg_static_flags.items;
tclsh_static_exe.root_module.addCSourceFile(.{ .file = b.path(src), .flags = flags });
}
tclsh_static_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/generic/tclMain.c"),
.flags = pkg_static_flags.items,
});
//static-build PLATFORMOBJS additions (registered as static packages)
tclsh_static_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclWinReg.c"),
.flags = stubext_static_flags.items,
});
tclsh_static_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclWinDde.c"),
.flags = stubext_static_flags.items,
});
tclsh_static_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclAppInit.c"),
.flags = appinit_static_flags.items,
});
inline for (.{ "tclStubLib", "tclTomMathStubLib", "tclOOStubLib" }) |nm| {
tclsh_static_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/generic/" ++ nm ++ ".c"),
.flags = stub_flags.items,
});
}
tclsh_static_exe.root_module.linkLibrary(zlib_lib_compile);
win_syslibs(tclsh_static_exe.root_module);
tclsh_static_exe.root_module.link_libc = true;
tclsh_static_exe.root_module.addWin32ResourceFile(.{
.file = tclshrc_overlay,
.include_paths = &.{
b.path(tcl_source_folder ++ "/generic"),
b.path(tcl_source_folder ++ "/win"),
},
});
const install_tclsh_static = b.addInstallArtifact(tclsh_static_exe, .{});
b.getInstallStep().dependOn(&install_tclsh_static.step);
// ================================================================
//tclsh86t.exe - the dynamic shell against tcl86t.dll (stock pair; proves
//the dll deliverable works)
const tclsh_dyn_exe = b.addExecutable(.{
.name = tclsh_dynamic,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
tclsh_dyn_exe.stack_size = 2300000;
tclsh_dyn_exe.subsystem = .Console; //see the static shell's subsystem note
core_include(tclsh_dyn_exe.root_module, b, tcluuid_include_dir);
tclsh_dyn_exe.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclAppInit.c"),
.flags = appinit_dyn_flags.items,
});
tclsh_dyn_exe.root_module.linkLibrary(tcl_lib_shared);
win_syslibs(tclsh_dyn_exe.root_module);
tclsh_dyn_exe.root_module.link_libc = true;
tclsh_dyn_exe.root_module.addWin32ResourceFile(.{
.file = tclshrc_overlay,
.include_paths = &.{
b.path(tcl_source_folder ++ "/generic"),
b.path(tcl_source_folder ++ "/win"),
},
});
const install_tclsh_dyn = b.addInstallArtifact(tclsh_dyn_exe, .{});
b.getInstallStep().dependOn(&install_tclsh_dyn.step);
// ================================================================
//dde/registry loadable packages (stubs consumers; installed into the script
//library's dde/ and reg/ dirs where library pkgIndexes expect them)
const dde_dll = b.addLibrary(.{
.linkage = .dynamic,
.name = std.fs.path.stem(dde_dll_file),
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
core_include(dde_dll.root_module, b, tcluuid_include_dir);
dde_dll.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclWinDde.c"),
.flags = stubext_flags.items,
});
dde_dll.root_module.linkLibrary(finalstublib);
win_syslibs(dde_dll.root_module);
dde_dll.root_module.link_libc = true;
const install_dde_dll = b.addInstallArtifact(dde_dll, .{
.dest_dir = .{ .override = .{ .custom = "./" ++ script_install_dir_rel ++ "/dde" } },
});
const reg_dll = b.addLibrary(.{
.linkage = .dynamic,
.name = std.fs.path.stem(reg_dll_file),
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
core_include(reg_dll.root_module, b, tcluuid_include_dir);
reg_dll.root_module.addCSourceFile(.{
.file = b.path(tcl_source_folder ++ "/win/tclWinReg.c"),
.flags = stubext_flags.items,
});
reg_dll.root_module.linkLibrary(finalstublib);
win_syslibs(reg_dll.root_module);
reg_dll.root_module.link_libc = true;
const install_reg_dll = b.addInstallArtifact(reg_dll, .{
.dest_dir = .{ .override = .{ .custom = "./" ++ script_install_dir_rel ++ "/reg" } },
});
// ================================================================
//thread 2.8 + tclvfs (both hook their installs onto the default install
//step inside the helpers)
const thread_build = try build_tclthread86(tcl_source_folder, "../tclthread", b, target, optimize, finalstublib);
const build_tclthread_step = b.step("build-tclthread", "build the thread 2.8 loadable extension");
build_tclthread_step.dependOn(&thread_build.lib.step);
const tclvfs_compile = try tclvfs86.build_tclvfs86(tcl_source_folder, "../tclvfs", b, target, optimize, finalstublib);
const build_tclvfs_step = b.step("build-tclvfs", "build the tclvfs loadable extension");
build_tclvfs_step.dependOn(&tclvfs_compile.step);
try tclvfs86.install_tclvfs_package("../tclvfs", b, tclvfs_compile);
// ================================================================
//install-libraries: the whole script library tree -> lib/tcl8.6 (each
//subdir package keeps its own pkgIndex; encoding/tzdata/msgs ride inside),
//dde/reg dlls into their package dirs, tm modules under lib/tcl8/<v>/ with
//versions parsed from the library pkgIndex files (makefile.vc nmakehlp -V
//parity). NOTE the whole-tree copy keeps http/msgcat/tcltest/platform as
//directory packages TOO (stock nmake installs those four as .tm only) -
//harmless duplication (same versions; the tm loader wins), simpler install.
const install_libraries = b.step("install-libraries", "install the tcl script library tree, dde/reg package dlls and tm modules");
const install_library_tree = b.addInstallDirectory(.{
.source_dir = b.path(tcl_source_folder ++ "/library"),
.install_dir = .prefix,
.install_subdir = script_install_dir_rel,
});
install_libraries.dependOn(&install_library_tree.step);
install_libraries.dependOn(&install_dde_dll.step);
install_libraries.dependOn(&install_reg_dll.step);
const ver_http = common.pkgIfneededVersion(b, tcl_source_folder ++ "/library/http/pkgIndex.tcl", "http");
const ver_msgcat = common.pkgIfneededVersion(b, tcl_source_folder ++ "/library/msgcat/pkgIndex.tcl", "msgcat");
const ver_tcltest = common.pkgIfneededVersion(b, tcl_source_folder ++ "/library/tcltest/pkgIndex.tcl", "tcltest");
const ver_platform = common.pkgIfneededVersion(b, tcl_source_folder ++ "/library/platform/pkgIndex.tcl", "platform");
const ver_pshell = common.pkgIfneededVersion(b, tcl_source_folder ++ "/library/platform/pkgIndex.tcl", "platform::shell");
std.debug.print("tm versions: http {s}, msgcat {s}, tcltest {s}, platform {s}, platform::shell {s}\n", .{ ver_http, ver_msgcat, ver_tcltest, ver_platform, ver_pshell });
const tm_installs = [_]struct { src: []const u8, subdir: []const u8, tmbase: []const u8, ver: []const u8 }{
.{ .src = "/library/http/http.tcl", .subdir = "8.6", .tmbase = "http", .ver = ver_http },
.{ .src = "/library/msgcat/msgcat.tcl", .subdir = "8.5", .tmbase = "msgcat", .ver = ver_msgcat },
.{ .src = "/library/tcltest/tcltest.tcl", .subdir = "8.5", .tmbase = "tcltest", .ver = ver_tcltest },
.{ .src = "/library/platform/platform.tcl", .subdir = "8.6", .tmbase = "platform", .ver = ver_platform },
.{ .src = "/library/platform/shell.tcl", .subdir = "8.6", .tmbase = "platform/shell", .ver = ver_pshell },
};
for (tm_installs) |tmi| {
const tm_inst = b.addInstallFileWithDir(b.path(b.fmt("{s}{s}", .{ tcl_source_folder, tmi.src })), .prefix, b.pathJoin(&.{ module_install_dir_rel, tmi.subdir, b.fmt("{s}-{s}.tm", .{ tmi.tmbase, tmi.ver }) }));
install_libraries.dependOn(&tm_inst.step);
}
// ================== post-build phases (built-shell runs) ==================
//Native-host only: these execute the built artifacts. All runs use the
//INSTALLED shells beside the installed lib tree with a scrubbed environment
//and NO TCL_LIBRARY - every run doubles as the hermetic-resolution proof the
//G-099 acceptance requires (exe/dll-relative ../lib/tcl8.6 discovery).
if (target.result.os.tag == builtin.os.tag) {
const installed_static = b.pathJoin(&.{ b.install_path, "bin", tclsh_static ++ ".exe" });
const installed_dyn = b.pathJoin(&.{ b.install_path, "bin", tclsh_dynamic ++ ".exe" });
const testreports_dir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "testreports" }), "\\", "/");
//-- smoke: both shells report the expected patchlevel and resolve the
//installed library (tzdata + autoload prove init.tcl + the tree)
const smoke_step = b.step("smoke", "smoke-test the installed shells (patchlevel + installed-library resolution + package loads)");
const shell_runs = [_]struct { exe: []const u8, label: []const u8 }{
.{ .exe = installed_static, .label = "static" },
.{ .exe = installed_dyn, .label = "dynamic" },
};
for (shell_runs) |sr| {
const smoke = b.addSystemCommand(&.{sr.exe});
common.scrubTclEnv(smoke);
smoke.has_side_effects = true;
smoke.setName(b.fmt("smoke {s}", .{sr.label}));
smoke.addFileArg(b.path("tools/suite_smoke.tcl"));
smoke.addArgs(&.{ "-mode", "installed", "-expect", tcl_h_patchlevel });
smoke.step.dependOn(b.getInstallStep());
smoke.step.dependOn(install_libraries);
smoke_step.dependOn(&smoke.step);
//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)
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.step.dependOn(b.getInstallStep());
pkgs.step.dependOn(install_libraries);
smoke_step.dependOn(&pkgs.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
//baseline file deliberately does not exist until the census is
//dispositioned - G-099).
const gate_step = b.step("test-gate", "tcl 8.6 core testsuite under the installed static shell, gated on parsed totals vs expected_test_failures.txt");
const testargs_opt = b.option([]const u8, "testargs", "extra tcltest args for test-gate (e.g -file http.test)") orelse "";
const testmode_opt = b.option([]const u8, "testmode", "core test-gate mode: gate|record (record = census run, no baseline needed)") orelse "gate";
const gate_run = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(gate_run);
gate_run.has_side_effects = true;
gate_run.setName("core test-gate");
gate_run.addFileArg(b.path("tools/test_gate.tcl"));
gate_run.addArgs(&.{ "-library", "tclcore", "-mode", testmode_opt });
gate_run.addArg("-testsdir");
gate_run.addArg(common.replaceAll(b, b.pathFromRoot(tcl_source_folder ++ "/tests"), "\\", "/"));
if (std.mem.eql(u8, testmode_opt, "gate")) {
gate_run.addArg("-baseline");
if (common.pathExists(b, "expected_test_failures.txt")) {
gate_run.addFileArg(b.path("expected_test_failures.txt"));
} else {
//missing baseline: pass the conventional path so the engine's
//message names the file to create after the record-mode census
gate_run.addArg(common.replaceAll(b, b.pathFromRoot("expected_test_failures.txt"), "\\", "/"));
}
}
gate_run.addArg("-logfile");
gate_run.addArg(common.replaceAll(b, b.pathFromRoot("../tcltest.log"), "\\", "/"));
gate_run.addArg("-summaryfile");
gate_run.addArg(b.fmt("{s}/tclcore.summary", .{testreports_dir}));
if (testargs_opt.len != 0) {
gate_run.addArgs(&.{ "-testargs", testargs_opt });
}
gate_run.step.dependOn(b.getInstallStep());
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.
// -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).
const LibSuite = struct {
name: []const u8,
testsdir: []const u8,
driver: []const u8,
default_policy: []const u8,
baseline: []const u8,
default_notfiles: []const u8,
};
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 = "" },
};
const libtests_step = b.step("test-libraries", "library testsuites (thread, tclvfs) under the installed static shell per policy tier");
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 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 (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 });
}
const run = b.addSystemCommand(&.{installed_static});
common.scrubTclEnv(run);
run.has_side_effects = true;
run.setName(b.fmt("test {s}", .{ls.name}));
run.addFileArg(b.path("tools/test_gate.tcl"));
run.addArgs(&.{ "-library", ls.name, "-mode", policy });
run.addArg("-testsdir");
run.addArg(common.replaceAll(b, b.pathFromRoot(ls.testsdir), "\\", "/"));
run.addArgs(&.{ "-driver", ls.driver });
if (std.mem.eql(u8, policy, "gate")) {
run.addArg("-baseline");
if (common.pathExists(b, ls.baseline)) {
run.addFileArg(b.path(ls.baseline));
} else {
run.addArg(common.replaceAll(b, b.pathFromRoot(ls.baseline), "\\", "/"));
}
}
run.addArg("-logfile");
run.addArg(b.fmt("{s}/{s}.log", .{ testreports_dir, ls.name }));
run.addArg("-summaryfile");
run.addArg(b.fmt("{s}/{s}.summary", .{ testreports_dir, ls.name }));
if (lib_testargs.len != 0) {
run.addArgs(&.{ "-testargs", lib_testargs });
}
if (lib_notfiles.len != 0) {
run.addArgs(&.{ "-notfiles", lib_notfiles });
}
run.step.dependOn(b.getInstallStep());
run.step.dependOn(install_libraries);
step.dependOn(&run.step);
}
}
}
//G-102 bootstrap mode (suite_tcl90 parity): registered when this recipe is
//invoked from the TRACKED suite directory. The stage layout matches suite.tcl's
//fossil staging so both flows share the staged invocation; the stage dir
//derives from THIS suite folder's name (copy-and-tweak isolation).
fn bootstrapMode(b: *std.Build) !void {
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "optimize mode forwarded to the staged build (default ReleaseFast)") orelse .ReleaseFast;
const suite_dirname = std.fs.path.basename(b.pathFromRoot(""));
const stage_rel = b.pathJoin(&.{ "..", "_build", suite_dirname });
const stage_tool = b.addExecutable(.{
.name = "stagetree",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/stagetree.zig"),
.target = b.graph.host,
}),
});
const stage_step = b.step("stage", "materialize pinned sources (build.zig.zon) + recipe copy into ../_build/<suite> (existing fossil/git checkouts left untouched)");
//dependency name (build.zig.zon) -> stage dir (the recipe-coupled dir names
//documented in sources.config)
const source_dirs = [_][2][]const u8{
.{ "tcl", "tcl86" },
.{ "tclthread", "tclthread" },
.{ "tclvfs", "tclvfs" },
};
var deps_pending = false;
for (source_dirs) |sd| {
const dep_name = sd[0];
const destdir = sd[1];
const dest_from_root = b.pathJoin(&.{ stage_rel, destdir });
//an existing fossil/git checkout is owned by the suite.tcl dev flow: skip
//the whole dependency so nothing is fetched for it (stagetree guards
//again at make time).
if (common.pathExists(b, b.pathJoin(&.{ dest_from_root, ".fslckout" })) or
common.pathExists(b, b.pathJoin(&.{ dest_from_root, "_FOSSIL_" })) or
common.pathExists(b, b.pathJoin(&.{ dest_from_root, ".git" })))
{
continue;
}
if (b.lazyDependency(dep_name, .{})) |dep| {
const run = b.addRunArtifact(stage_tool);
run.setName(b.fmt("stage {s}", .{destdir}));
run.has_side_effects = true;
//the dependency's content-addressed cache path doubles as the pin
//key: a manifest pin bump invalidates the materialized copy
const dep_root = dep.path("").getPath(b);
run.addArgs(&.{ "-key", dep_root, "-src", dep_root, "-dest", b.pathFromRoot(dest_from_root) });
stage_step.dependOn(&run.step);
} else {
deps_pending = true;
}
}
if (deps_pending) return; //zig fetches the lazy deps, then re-runs configure
//recipe copy -> <stage>/build (the same item list suite.tcl stages)
const recipe_items = [_][]const u8{
"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",
};
for (recipe_items) |item| {
const run = b.addRunArtifact(stage_tool);
run.setName(b.fmt("stage recipe {s}", .{item}));
run.has_side_effects = true;
run.addArgs(&.{ "-key", "-", "-src", b.pathFromRoot(item), "-dest", b.pathFromRoot(b.pathJoin(&.{ stage_rel, "build", item })) });
stage_step.dependOn(&run.step);
}
pub fn build(b: *std.Build) void {
_ = b;
@compileError("suite_tcl86 recipe derivation in progress (G-099) - sources are staged; see suite_tcl86/README.md DERIVATION for the makefile.vc-derived plan and goals/G-099-suite-tcl86-buildsuite.md for progress");
//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" });
var cachever: []const u8 = builtin.zig_version_string;
cachever = common.replaceAll(b, cachever, "+", "_");
cachever = common.replaceAll(b, cachever, "/", "_");
cachever = common.replaceAll(b, cachever, ":", "_");
const nested = b.addSystemCommand(&.{
b.graph.zig_exe,
"build",
"--build-file",
common.replaceAll(b, b.pathFromRoot(b.pathJoin(&.{ stage_rel, "build", "build86.zig" })), "\\", "/"),
"--cache-dir",
common.replaceAll(b, b.pathFromRoot(b.pathJoin(&.{ stage_rel, "build", b.fmt(".zig-cache-{s}", .{cachever}) })), "\\", "/"),
"--prefix",
common.replaceAll(b, b.pathFromRoot(b.pathJoin(&.{ stage_rel, "out" })), "\\", "/"),
b.fmt("-Doptimize={s}", .{@tagName(optimize)}),
});
for (steps_opt) |s| nested.addArg(s);
nested.setName("staged build (nested zig build)");
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| {
nested.removeEnvironmentVariable(ev);
}
nested.step.dependOn(stage_step);
const bootstrap_step = b.step("bootstrap", "stage + run the staged recipe end-to-end (no pre-existing tclsh required)");
bootstrap_step.dependOn(&nested.step);
}

141
src/buildsuites/suite_tcl86/build_common.zig

@ -0,0 +1,141 @@
//build_common.zig - shared configure-time helpers for the suite recipe files.
//Forked from suite_tcl90/build_common.zig (G-099 duplication-with-a-note posture)
//plus the 8.6-only pkgIfneededVersion parser (tm module versions derive from the
//library pkgIndex files, mirroring makefile.vc's nmakehlp -V derivation).
//These run while the build graph is being CONSTRUCTED (not as build steps). Reads
//are of staged/fetched source trees; generated content is never written into a
//source tree - it goes through b.addWriteFiles() overlays into the build cache and
//is consumed via overlay include dirs / overlay rc copies.
const std = @import("std");
pub fn pathExists(b: *std.Build, path_from_root: []const u8) bool {
const abs = b.pathFromRoot(path_from_root);
std.Io.Dir.cwd().access(b.graph.io, abs, .{}) catch return false;
return true;
}
pub fn readSourceFile(b: *std.Build, path_from_root: []const u8) []u8 {
const abs = b.pathFromRoot(path_from_root);
return std.Io.Dir.cwd().readFileAlloc(b.graph.io, abs, b.allocator, .limited(8 * 1024 * 1024)) catch |err| {
std.debug.panic("suite recipe: cannot read {s}: {s}", .{ abs, @errorName(err) });
};
}
//value of a '#define <name> "<value>"' line (e.g TCL_PATCH_LEVEL from tcl.h)
pub fn parseDefineString(content: []const u8, name: []const u8) ?[]const u8 {
var it = std.mem.splitScalar(u8, content, '\n');
while (it.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
if (!std.mem.startsWith(u8, trimmed, "#")) continue;
const after_hash = std.mem.trimStart(u8, trimmed[1..], " \t");
if (!std.mem.startsWith(u8, after_hash, "define")) continue;
const after_def = std.mem.trimStart(u8, after_hash[6..], " \t");
if (!std.mem.startsWith(u8, after_def, name)) continue;
const after_name = after_def[name.len..];
if (after_name.len == 0 or (after_name[0] != ' ' and after_name[0] != '\t')) continue;
const q1 = std.mem.indexOfScalar(u8, after_name, '"') orelse continue;
const rest = after_name[q1 + 1 ..];
const q2 = std.mem.indexOfScalar(u8, rest, '"') orelse continue;
return rest[0..q2];
}
return null;
}
pub fn replaceAll(b: *std.Build, input: []const u8, needle: []const u8, replacement: []const u8) []u8 {
return std.mem.replaceOwned(u8, b.allocator, input, needle, replacement) catch @panic("OOM");
}
//hermetic child shells: steps that run the suite-built tclsh must not see the
//user's machine-level tcl environment (TCLLIBPATH pushing machine package dirs
//misdirects installer default paths and risks false-positive smokes; TCL_LIBRARY/
//TK_LIBRARY misdirect init script resolution). The G-099 acceptance requires
//TCLLIBPATH/TCL_LIBRARY unset in ALL suite child invocations - the 8.6 recipe
//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| {
run.removeEnvironmentVariable(ev);
}
}
//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.
pub fn manifestUuid(b: *std.Build, tree_from_root: []const u8) []const u8 {
const abs = b.pathFromRoot(b.fmt("{s}/manifest.uuid", .{tree_from_root}));
const data = std.Io.Dir.cwd().readFileAlloc(b.graph.io, abs, b.allocator, .limited(4096)) catch return "unrecorded";
return std.mem.trim(u8, data, " \t\r\n");
}
//package version from a TEA tree's configure.ac 'AC_INIT([name],[version])'
//line (thread, tclvfs, ...). Version-derived artifact names must follow the
//checkout - hardcoded versions stamp mismatched sources (suite_tcl90's G-107
//finding: a thread dll self-reporting the wrong version could never satisfy the
//testsuite's exact-version require).
pub fn acInitVersion(b: *std.Build, tree_from_root: []const u8) []const u8 {
const data = readSourceFile(b, b.fmt("{s}/configure.ac", .{tree_from_root}));
var it = std.mem.splitScalar(u8, data, '\n');
while (it.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
if (!std.mem.startsWith(u8, trimmed, "AC_INIT")) continue;
//the second [...] group is the version
var rest = trimmed;
var group: usize = 0;
while (std.mem.indexOfScalar(u8, rest, '[')) |open| {
const after = rest[open + 1 ..];
const close = std.mem.indexOfScalar(u8, after, ']') orelse break;
group += 1;
if (group == 2) return b.dupe(after[0..close]);
rest = after[close + 1 ..];
}
}
std.debug.panic("no AC_INIT version in {s}/configure.ac", .{tree_from_root});
}
pub fn stripChars(b: *std.Build, input: []const u8, drop: []const u8) []u8 {
var out = std.array_list.Managed(u8).init(b.allocator);
for (input) |ch| {
if (std.mem.indexOfScalar(u8, drop, ch) != null) continue;
out.append(ch) catch @panic("OOM");
}
return out.items;
}
//8.6 tm module versions: 'package ifneeded <name> <version> ...' from a library
//pkgIndex.tcl (makefile.vc derives PKG_HTTP_VER etc the same way via nmakehlp -V).
//Exact token match on the package name distinguishes 'platform' from
//'platform::shell'.
pub fn pkgIfneededVersion(b: *std.Build, pkgindex_from_root: []const u8, pkgname: []const u8) []const u8 {
const data = readSourceFile(b, pkgindex_from_root);
var lines = std.mem.splitScalar(u8, data, '\n');
while (lines.next()) |line| {
var toks = std.mem.tokenizeAny(u8, line, " \t\r");
const t0 = toks.next() orelse continue;
if (!std.mem.eql(u8, t0, "package")) continue;
const t1 = toks.next() orelse continue;
if (!std.mem.eql(u8, t1, "ifneeded")) continue;
const t2 = toks.next() orelse continue;
if (!std.mem.eql(u8, t2, pkgname)) continue;
const ver = toks.next() orelse continue;
return b.dupe(ver);
}
std.debug.panic("no 'package ifneeded {s}' in {s}", .{ pkgname, pkgindex_from_root });
}
//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)
pub fn winResourceVersion(b: *std.Build, dotversion: []const u8, patchlevel: []const u8) []u8 {
const rlevel: u8 = if (std.mem.indexOfScalar(u8, patchlevel, 'a') != null)
'0'
else if (std.mem.indexOfScalar(u8, patchlevel, 'b') != null)
'1'
else
'2';
var stripped = std.array_list.Managed(u8).init(b.allocator);
for (patchlevel) |ch| {
if (ch == 'a' or ch == 'b' or ch == '.') continue;
stripped.append(ch) catch @panic("OOM");
}
return std.fmt.allocPrint(b.allocator, "{s}.{c}.{s}", .{ dotversion, rlevel, stripped.items }) catch @panic("OOM");
}

137
src/buildsuites/suite_tcl86/build_tclthread86/build_tclthread86.zig

@ -0,0 +1,137 @@
const std = @import("std");
const common = @import("../build_common.zig");
//build_tclthread86 - thread 2.8 (thread-2-8-branch) loadable extension for the
//8.6 suite (G-099). Forked from suite_tcl90/build_tclthread (duplication-with-a-
//note posture) with the 2.8/8.6 differences:
// - dll name follows the nmake TEA convention for a tcl86 host: thread<VER>t
// (rules.vc keeps the 't' suffix while TCL_THREADS && TCL_VERSION <= 86) -
// e.g thread2813t.dll.
// - pkgIndex.tcl is GENERATED FROM THE CHECKOUT'S OWN pkgIndex.tcl.in
// (@PACKAGE_VERSION@/@PKG_LIB_FILE@/@PACKAGE_NAME@ substitution): 2.8's
// index carries the capital-'Thread' ifneeded plus the Ttrace package
// wiring, and loads the dll $dir-relative (TEA installed shape - the dll
// lives INSIDE lib/thread<version>/, not in bin/ as the 9.0 fork's
// hand-written index arranged).
// - lib/ttrace.tcl installs into the package dir (the generated index's
// '[file join $dir ttrace.tcl]' fallback branch).
// - no thread.rc in the 2-8 branch: no version resource step.
// - flags: TCL_THREADS=1 (8.6-era extension define); the 9.0 fork's
// ZIPFS_BUILD / TCL_WIDE_INT_TYPE defines dropped (9-isms; tcl.h derives
// wide ints itself under __GNUC__).
//Version-derived names follow the checkout's AC_INIT (suite_tcl90 G-107
//lesson: hardcoded versions stamp mismatched sources).
pub const TclThreadBuild = struct {
lib: *std.Build.Step.Compile,
pkgidx: std.Build.LazyPath,
version: []const u8, //e.g "2.8.13" (from the checkout's AC_INIT)
dll_file: []const u8, //e.g "thread2813t.dll"
pkgsub: []const u8, //e.g "lib/thread2.8.13" (prefix-relative package dir)
};
pub fn build_tclthread86(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !TclThreadBuild {
const thread_version = common.acInitVersion(b, subdir);
const thread_vernodots = common.stripChars(b, thread_version, ".");
const pkg_lib_name = b.fmt("thread{s}t", .{thread_vernodots});
const pkg_lib_file = b.fmt("{s}.dll", .{pkg_lib_name});
const pkgsub = b.fmt("lib/thread{s}", .{thread_version});
//threadUuid.h (threadCmd.c #includes it; upstream makefiles generate it into
//their build dir) - generated into the cache, delivered via an include-dir
//overlay that also carries the dependency on the generation step.
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 THREAD_VERSION_UUID \\",
"-prefixnl",
"1",
"-input",
});
uuidcmd_step.addFileArg(b.path(subdir ++ "/manifest.uuid"));
uuidcmd_step.addArg("-output");
const threaduuid_file = uuidcmd_step.addOutputFileArg("threadUuid.h");
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = pkg_lib_name,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
var flags = std.array_list.Managed([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{ "-Wall", "-O3", "-fomit-frame-pointer" });
try flags.appendSlice(&.{
"-DPACKAGE_NAME=\"thread\"",
"-DPACKAGE_TARNAME=\"thread\"",
b.fmt("-DPACKAGE_VERSION=\"{s}\"", .{thread_version}),
b.fmt("-DPACKAGE_STRING=\"thread {s}\"", .{thread_version}),
"-DPACKAGE_BUGREPORT=\"\"",
"-DPACKAGE_URL=\"\"",
"-DTCL_CFG_OPTIMIZED=1",
"-DUSE_TCL_STUBS=1",
"-DTCL_THREADS=1",
"-DHAVE_INTTYPES_H=1",
"-static-libgcc",
});
if (target.result.os.tag == .windows) {
try flags.appendSlice(&.{
"-DBUILD_thread=/**/",
});
}
//thread-2-8-branch win/makefile.vc PRJ_OBJS (same 10-file set as trunk)
var tclthread_sources = std.array_list.Managed([]const u8).init(b.allocator);
try tclthread_sources.append(subdir ++ "/generic/threadNs.c");
try tclthread_sources.append(subdir ++ "/generic/threadCmd.c");
try tclthread_sources.append(subdir ++ "/generic/threadSvCmd.c");
try tclthread_sources.append(subdir ++ "/generic/threadSpCmd.c");
try tclthread_sources.append(subdir ++ "/generic/threadPoolCmd.c");
try tclthread_sources.append(subdir ++ "/generic/psGdbm.c");
try tclthread_sources.append(subdir ++ "/generic/psLmdb.c");
try tclthread_sources.append(subdir ++ "/generic/threadSvListCmd.c");
try tclthread_sources.append(subdir ++ "/generic/threadSvKeylistCmd.c");
try tclthread_sources.append(subdir ++ "/generic/tclXkeylist.c");
lib.root_module.addIncludePath(b.path(subdir));
lib.root_module.addIncludePath(b.path(subdir ++ "/generic"));
lib.root_module.addIncludePath(threaduuid_file.dirname());
lib.root_module.addIncludePath(b.path(tcldir ++ "/generic"));
lib.root_module.addCSourceFiles(.{
.files = tclthread_sources.items,
.flags = flags.items,
});
lib.root_module.linkLibrary(stublib);
lib.root_module.link_libc = true;
//installed shape: lib/thread<version>/{<dll>, pkgIndex.tcl, ttrace.tcl}
const install_dll = b.addInstallArtifact(lib, .{
.dest_dir = .{ .override = .{ .custom = pkgsub } },
});
b.getInstallStep().dependOn(&install_dll.step);
//pkgIndex.tcl from the checkout's own template (capital-'Thread' ifneeded +
//Ttrace wiring ride along)
var pkgidx_content: []const u8 = common.readSourceFile(b, subdir ++ "/pkgIndex.tcl.in");
pkgidx_content = common.replaceAll(b, pkgidx_content, "@PACKAGE_VERSION@", thread_version);
pkgidx_content = common.replaceAll(b, pkgidx_content, "@PKG_LIB_FILE@", pkg_lib_file);
pkgidx_content = common.replaceAll(b, pkgidx_content, "@PACKAGE_NAME@", "thread");
const wf_pkgidx = b.addWriteFiles();
const pkgidx = wf_pkgidx.add("pkgIndex.tcl", pkgidx_content);
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, b.fmt("{s}/pkgIndex.tcl", .{pkgsub}));
b.getInstallStep().dependOn(&pkgidx_install.step);
const ttrace_install = b.addInstallFileWithDir(b.path(subdir ++ "/lib/ttrace.tcl"), .prefix, b.fmt("{s}/ttrace.tcl", .{pkgsub}));
b.getInstallStep().dependOn(&ttrace_install.step);
return .{ .lib = lib, .pkgidx = pkgidx, .version = thread_version, .dll_file = pkg_lib_file, .pkgsub = pkgsub };
}

161
src/buildsuites/suite_tcl86/build_tclvfs86/build_tclvfs86.zig

@ -0,0 +1,161 @@
const std = @import("std");
const common = @import("../build_common.zig");
//build_tclvfs86 - tclvfs (TRUNK, 1.4.2 era) loadable extension for the 8.6
//suite (G-099). Forked from suite_tcl90/build_tclvfs/build_tclvfs_shared.zig
//(duplication-with-a-note posture) with the 8.6 differences:
// - the dll is BUILT UNDER THE TCL8 TEA-NMAKE NAME (vfs<nodots>t.dll, e.g
// vfs142t.dll - rules.vc PRJLIBNAME8 with PROJECT=vfs, SUFX 't' kept on a
// tcl86 host): the configured library/vfs.tcl.in loads @PKG_LIB_FILE8@
// with NO explicit init symbol, so Tcl's load-time package-name guessing
// derives the _Init name from the file's alpha prefix - 'vfs...' guesses
// Vfs_Init (correct); a 'tclvfs...' spelling guesses Tclvfs_Init and
// fails. @PKG_LIB_FILE9@ is still substituted so the script is complete.
// - the 9.0 fork's ZIPFS_BUILD / TCL_WIDE_INT_TYPE defines dropped (9-isms).
// - trunk vfs.c uses the same lowercase BUILD_vfs export guard as forTcl9
// (verified in the staged tree), so the guard define carries over.
//The include-order shim carries over: under zig's mingw headers '__stat64' is
//a MACRO for _stat64 (_mingw_stat64.h, pulled in by <sys/stat.h>); tcl.h's
//'typedef struct __stat64 Tcl_StatBuf' parsed BEFORE that include binds a raw
//forever-incomplete __stat64 tag (8.6 tcl.h uses the same typedef under
//_WIN64). Compiling a generated TU that includes <sys/stat.h> first makes the
//macro visible when tcl.h parses.
pub const TclvfsInfo = struct {
dotversion: []const u8, //e.g "1.4.2"
nodots: []const u8, //e.g "142"
dll_file: []const u8, //the tcl8-named dll this suite builds, e.g "vfs142t.dll"
dll9_file: []const u8, //tcl9 spelling for template substitution completeness
major: []const u8,
minor: []const u8,
};
pub fn versionInfo(b: *std.Build, comptime tree_from_root: []const u8) TclvfsInfo {
const dotversion = common.acInitVersion(b, tree_from_root);
const nodots = common.stripChars(b, dotversion, ".");
var parts = std.mem.splitScalar(u8, dotversion, '.');
const major = parts.next() orelse "0";
const minor = parts.next() orelse "0";
return .{
.dotversion = dotversion,
.nodots = nodots,
.dll_file = b.fmt("vfs{s}t.dll", .{nodots}),
.dll9_file = b.fmt("tcl9vfs{s}.dll", .{nodots}),
.major = b.dupe(major),
.minor = b.dupe(minor),
};
}
//configure-products (vfs.tcl, pkgIndex.tcl) generated from their .in templates
//at configure time.
fn configuredContent(b: *std.Build, comptime subdir: []const u8, comptime template: []const u8, info: TclvfsInfo) []const u8 {
var t: []const u8 = common.readSourceFile(b, subdir ++ "/" ++ template);
t = common.replaceAll(b, t, "@PACKAGE_VERSION@", info.dotversion);
t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", info.dll9_file);
t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", info.dll_file);
return t;
}
//install the tclvfs script package (lib/vfs<ver>) into the prefix - the two
//generated configure-products, the library scripts + template dir copied, and
//the dll alongside (vfs.tcl loads it from its own dir via ::vfs::self).
pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs_compile: *std.Build.Step.Compile) !void {
const info = versionInfo(b, subdir);
const pkgsub = b.fmt("lib/vfs{s}", .{info.dotversion});
const install_scripts = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library"),
.install_dir = .prefix,
.install_subdir = pkgsub,
.include_extensions = &.{".tcl"},
});
b.getInstallStep().dependOn(&install_scripts.step);
//template dir wholesale (carries non-.tcl support files such as tclIndex)
const install_template = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library/template"),
.install_dir = .prefix,
.install_subdir = b.fmt("{s}/template", .{pkgsub}),
});
b.getInstallStep().dependOn(&install_template.step);
const wf = b.addWriteFiles();
inline for (.{
.{ "library/vfs.tcl.in", "vfs.tcl" },
.{ "pkgIndex.tcl.in", "pkgIndex.tcl" },
}) |pair| {
const gen = wf.add(pair[1], configuredContent(b, subdir, pair[0], info));
const inst = b.addInstallFileWithDir(gen, .prefix, b.fmt("{s}/{s}", .{ pkgsub, pair[1] }));
b.getInstallStep().dependOn(&inst.step);
}
const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, b.fmt("{s}/{s}", .{ pkgsub, info.dll_file }));
b.getInstallStep().dependOn(&dll_inst.step);
}
pub fn build_tclvfs86(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !*std.Build.Step.Compile {
const info = versionInfo(b, subdir);
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = b.fmt("vfs{s}t", .{info.nodots}),
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
var flags = std.array_list.Managed([]const u8).init(b.allocator);
defer flags.deinit();
try flags.appendSlice(&.{ "-Wall", "-O3", "-fomit-frame-pointer" });
try flags.appendSlice(&.{
"-DPACKAGE_NAME=\"vfs\"",
"-DPACKAGE_TARNAME=\"vfs\"",
b.fmt("-DPACKAGE_VERSION=\"{s}\"", .{info.dotversion}),
b.fmt("-DPACKAGE_STRING=\"vfs {s}\"", .{info.dotversion}),
"-DPACKAGE_BUGREPORT=\"\"",
"-DPACKAGE_URL=\"\"",
//vfs.c guards <sys/stat.h> paths behind HAVE_SYS_STAT_H and needs the
//COMPLETE Tcl_StatBuf - see the include-order shim below.
"-DHAVE_SYS_STAT_H=1",
"-DTCL_CFG_OPTIMIZED=1",
"-DUSE_TCL_STUBS=1",
});
if (target.result.os.tag == .windows) {
try flags.appendSlice(&.{
"-DBUILD_vfs=/**/",
});
}
lib.root_module.addIncludePath(b.path(subdir));
lib.root_module.addIncludePath(b.path(subdir ++ "/generic"));
lib.root_module.addIncludePath(b.path(tcldir ++ "/generic"));
if (target.result.os.tag == .windows) {
lib.root_module.addIncludePath(b.path(tcldir ++ "/win"));
} else {
lib.root_module.addIncludePath(b.path(tcldir ++ "/unix"));
}
//Include-order shim (overlay TU - source trees never written): see the
//header comment for the __stat64 macro-vs-tag story.
const wf_shim = b.addWriteFiles();
const vfs_shim = wf_shim.add("vfs_statorder_shim.c",
\\/* generated include-order shim - see build_tclvfs86.zig */
\\#include <sys/stat.h>
\\#include "vfs.c"
\\
);
lib.root_module.addCSourceFile(.{
.file = vfs_shim,
.flags = flags.items,
});
lib.root_module.linkLibrary(stublib);
lib.root_module.link_libc = true;
lib.root_module.addWin32ResourceFile(.{
.file = b.path(subdir ++ "/win/tclvfs.rc"),
.flags = &.{
b.fmt("-DPACKAGE_MAJOR={s}", .{info.major}),
b.fmt("-DPACKAGE_MINOR={s}", .{info.minor}),
b.fmt("-DPACKAGE_VERSION={s}", .{info.dotversion}),
b.fmt("-DDOTVERSION={s}", .{info.dotversion}),
b.fmt("-DCOMMAVERSION={s}", .{info.dotversion}),
b.fmt("-DVERSION={s}", .{info.nodots}),
},
}); //ignored for non-windows targets
return lib;
}

50
src/buildsuites/suite_tcl86/build_zlib86/build_zlib86.zig

@ -0,0 +1,50 @@
const std = @import("std");
//build_zlib86 - static zlib for the 8.6 core (G-099). 8.6's win/makefile.vc does
//NOT compile zlib for shared builds - it links a PREBUILT compat/zlib/win64/
//zdll.lib and ships zlib1.dll beside the core dll; only STATIC nmake builds
//compile zlib sources in (the 11-file ZLIBOBJS list). The zig-only recipe
//compiles that same source list into a static lib linked into BOTH the tcl86
//dll and the static shell, eliminating the shipped zlib1.dll (deviation from
//stock shared-layout recorded in README.md DERIVATION).
//
//Leaner than suite_tcl90's build_zlib (no zlib1.dll product, no minigzip/example
//test exes, no directory iteration - the exact makefile.vc ZLIBOBJS list).
pub fn build_zlib86(comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !*std.Build.Step.Compile {
const statlib = b.addLibrary(.{
.linkage = .static,
.name = "z86",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
var zlib_flags = std.array_list.Managed([]const u8).init(b.allocator);
defer zlib_flags.deinit();
//flag set proven by suite_tcl90's zlib compile under zig cc
try zlib_flags.appendSlice(&.{ "-Wall", "-Wno-error=implicit-function-declaration", "-O3", "-fPIC", "-fno-omit-frame-pointer", "-fno-sanitize=undefined" });
//makefile.vc ZLIBOBJS (the static-build compile list)
const zlib_sources = [_][]const u8{
subdir ++ "/adler32.c",
subdir ++ "/compress.c",
subdir ++ "/crc32.c",
subdir ++ "/deflate.c",
subdir ++ "/infback.c",
subdir ++ "/inffast.c",
subdir ++ "/inflate.c",
subdir ++ "/inftrees.c",
subdir ++ "/trees.c",
subdir ++ "/uncompr.c",
subdir ++ "/zutil.c",
};
var sources = std.array_list.Managed([]const u8).init(b.allocator);
try sources.appendSlice(&zlib_sources);
statlib.root_module.link_libc = true;
statlib.root_module.addIncludePath(b.path(subdir));
statlib.root_module.addCSourceFiles(.{
.files = sources.items,
.flags = zlib_flags.items,
});
return statlib;
}

23
src/buildsuites/suite_tcl86/expected_test_failures.txt

@ -0,0 +1,23 @@
# suite_tcl86 expected Tcl-core-test failures (G-099 test gate baseline).
# One test name per line; comments carry the disposition reason. 'suite.tcl test'
# (zig build test-gate, mode gate) fails on any failed test NOT listed here, and
# notes listed tests that no longer fail (candidates for removal).
#
# Baseline established 2026-07-26 against tcl core-8-6-branch 1d1d5cbd91eb
# (8.6.18), zig 0.16.0 suite build (tclsh86ts.exe static shell), machine SuperBee
# (windows 11). Full-run totals at capture: 46525 run / 39227 passed / 7296
# skipped (constraint-driven: serverNeeded 1996, testregexp 1099, testchannel
# 859, ... the test* internal-command constraints are off - tcltest built without
# the tcltest C package) / 2 failed, both dispositioned below.
#
# The suite_tcl90 (9.0.5) fCmd-31.x/32.x 'file home' machine-trait failures do
# NOT appear here: 'file home' is a Tcl 8.7/9.0 command, absent in 8.6.
# -- machine trait: windows admin-share self-globbing. The test globs
# //<hostname>/c/*Test expecting exactly one match (globTest). On this machine
# several *Test/*test dirs exist at C:\ (buildtest, globTest, notes_test,
# tcltest, test, _test), so the case-insensitive glob returns all of them. Fails
# on this machine's directory layout regardless of the runtime (suite_tcl90's
# 9.0.5 baseline dispositions the same two names for the same reason).
filename-16.12
filename-16.13

26
src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt

@ -0,0 +1,26 @@
# suite_tcl86 expected tclvfs-testsuite failures (G-099 library test gate baseline).
# One test name per line; comments carry the disposition reason. The test-tclvfs
# step (policy gate) fails on any failed test NOT listed here, and notes listed
# tests that no longer fail (candidates for removal).
#
# Baseline established 2026-07-26 against tclvfs trunk f082c47f9b2e (vfs 1.4.2
# era; the live dev-flow trunk tip is ahead of the build.zig.zon pin 9954261983
# - both are trunk/1.4.2, the pin is the no-tclsh flow's provenance record),
# suite-built tclsh86ts.exe 8.6.18, machine SuperBee (windows 11). Census totals
# at capture: 51 run / 41 passed / 8 skipped (constraint-driven: ftp 3,
# bug1533748 2, zipcat 2, knownBug 1) / 2 failed, both dispositioned below.
#
# suite_tcl90 moved tclvfs to the forTcl9 branch (1.5.0) where these two
# network-reaching vfsUrl entries SKIP; on trunk (1.4.2) they run and fail
# 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
# (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.)
vfsUrl-1.2
vfsUrl-2.1

13
src/buildsuites/suite_tcl86/expected_test_failures_thread.txt

@ -0,0 +1,13 @@
# suite_tcl86 expected thread-testsuite failures (G-099 library test gate baseline).
# One test name per line; comments carry the disposition reason. The test-thread
# step (policy gate) fails on any failed test NOT listed here, and notes listed
# tests that no longer fail (candidates for removal).
#
# Baseline established 2026-07-26 against thread thread-2-8-branch e14fa771ae2e
# (thread 2.8.13), suite-built tclsh86ts.exe 8.6.18, machine SuperBee (windows
# 11). Census totals at capture: 142 run / 126 passed / 16 skipped / 0 failed -
# EMPTY baseline; skips are constraint-driven (chanTransfer windows, have_gdbm/
# have_lmdb backends not built into the suite's psGdbm/psLmdb compile - same
# constraint profile as suite_tcl90's thread 3.0.7 census).
#
# (no expected failures)

8
src/buildsuites/suite_tcl86/sources.config

@ -28,10 +28,10 @@
# - 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).
description Zig-built Tcl 8.6 windows runtime (core-8-6-branch): tclsh86 + tcl86 dll + installed lib tree, thread 2.8 + tclvfs, core-test gate (G-099 arc - recipe derivation in progress)
product tclsh86 executable + tcl86 dll with installed lib tree (no zipfs in 8.6 - no self-contained szip runtime; kit containment is G-101 territory)
product 8.6-compatible thread (thread-2-8-branch, 2.8.13 era) and tclvfs (trunk, 1.4.2 era) built and loadable in that shell
product core testsuite gate vs a tracked expected_test_failures baseline (parsed all.tcl totals; first census pending - see README)
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)
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)
doc README.md
zigpin bin/tools/zig-x86_64-windows-0.16.0/zig.exe

4
src/buildsuites/suite_tcl86/src/main.zig

@ -0,0 +1,4 @@
const std = @import("std");
pub const _start = void;
pub const WinMainCRTStartup = void;

22
src/buildsuites/suite_tcl86/suite.tcl

@ -6,15 +6,9 @@
#LIVE fossil checkouts, and delegates the build/test pipeline to the staged zig
#recipe's steps.
#
#RECIPE STATUS (G-099): build86.zig is a pending-derivation skeleton - it fails
#with a clear message after source staging succeeds. Staging alone is useful: it
#materializes the core-8-6-branch checkout (win/makefile.vc etc) the derivation
#studies. See README.md DERIVATION.
#
#This is the FOSSIL DEV FLOW - convenient live-branch work against checkouts. The
#equivalent no-tclsh flow ('zig build stage|bootstrap' fetching the build.zig.zon
#pins) lands with the recipe; the zon pins are recorded from this suite's first
#commit.
#equivalent no-tclsh flow is 'zig build stage|bootstrap' from the suite dir
#(build86.zig bootstrap mode fetching the build.zig.zon pins).
#
#Usage: tclsh suite.tcl build ?options?
# tclsh suite.tcl test ?-testargs {tcltest args}?
@ -29,10 +23,12 @@
# -repofolder <dir> fossil clone store (default ~/.punkshell/fossils; env PUNK_FOSSIL_STORE)
# -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} (grows with the
# recipe: thread/tclvfs installs, smoke, per G-099).
# -steps <list> zig build steps. 'build' default: {install install-libraries
# smoke} (runtimes + companions + script library + smokes).
# 'test' default: {test-gate} (the 8.6 core suite gate vs the
# tracked expected_test_failures baseline - pending first census).
# tracked expected_test_failures baseline; first census via
# -zigargs {-Dtestmode=record}). Also: test-thread, test-tclvfs,
# test-libraries (G-107-pattern policy tiers).
# -zigargs <list> extra args appended to the zig build invocation
#
#The build area lives entirely under src/buildsuites/_build (VCS-ignored). Fossil
@ -123,7 +119,7 @@ array set opt {
-optimize ReleaseFast
-tclbranch {}
-refresh 0
-steps {install}
-steps {install install-libraries smoke}
-testargs {}
-zigargs {}
-repofolder {}
@ -286,7 +282,7 @@ proc fossil_source {name url branch dir} {
#recently-used cache trees).
log "staging recipe -> $builddir"
file mkdir $builddir
foreach item {build86.zig} {
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} {
file delete -force [file join $builddir $item]
file copy [file join $suiteroot $item] $builddir
}

42
src/buildsuites/suite_tcl86/tools/pkg_smoke.tcl

@ -0,0 +1,42 @@
#pkg_smoke.tcl (G-102): package-resolution smoke run under a suite-built shell.
#Requires each named package against the shell's own installed prefix, printing
#name+version; -gui 1 wraps the requires in a withdrawn Tk root (for tklib
#packages that need a display); -accel <pkg> additionally asserts the package's
#critcl accelerator engaged (::<pkg>::accel(critcl) == 1, tcllibc proof).
#Formerly suite.tcl inline smokes.
#
#args: ?-gui 0|1? ?-accel <pkg>? package ?package ...?
proc fail {msg} {puts stderr "pkg_smoke FAIL: $msg"; flush stderr; exit 1}
set gui 0
set accel {}
set pkgs {}
for {set i 0} {$i < [llength $argv]} {incr i} {
set a [lindex $argv $i]
switch -- $a {
-gui {incr i; set gui [lindex $argv $i]}
-accel {incr i; set accel [lindex $argv $i]}
default {lappend pkgs $a}
}
}
if {![llength $pkgs] && $accel eq ""} {fail "no packages given"}
if {$gui} {
if {[catch {package require Tk; wm withdraw .} err]} {fail "Tk load failed: $err"}
}
set report {}
foreach pkg $pkgs {
if {[catch {package require $pkg} ver]} {fail "package require $pkg failed: $ver"}
lappend report "$pkg:$ver"
}
if {$accel ne ""} {
if {[catch {package require $accel} err]} {fail "package require $accel (accel host) failed: $err"}
upvar #0 ::${accel}::accel accelarr
if {![info exists accelarr(critcl)] || !$accelarr(critcl)} {
fail "critcl accelerator not engaged for $accel (tcllibc not loading?)"
}
lappend report "${accel}-critcl-accel:1"
}
if {$gui} {catch {destroy .}}
puts "pkg_smoke OK: [join $report { }]"
exit 0

173
src/buildsuites/suite_tcl86/tools/stagetree.zig

@ -0,0 +1,173 @@
//stagetree - materialize a source tree (or single file) into the build stage (G-102).
//
//Used by the suite recipe's bootstrap mode to copy zon-fetched dependency trees and
//the tracked recipe files into the _build/<suite> stage layout that the staged build
//invocation (and the fossil dev flow) share. Invariant guarded here: an existing
//fossil/git CHECKOUT at the destination is never touched - the fossil dev flow owns
//those directories.
//
// stagetree -key <pin|-> -src <path> -dest <path>
//
// -key <pin> pinned-tree mode: if <dest>/.stage-materialized already contains
// exactly <pin>, the destination is up to date and nothing is done.
// Otherwise the destination tree is DELETED, the source tree copied,
// and the marker written. Pass the dependency's content-addressed
// cache path as the pin - a pin bump changes it, invalidating the copy.
// -key - always mode (recipe files): delete destination, copy, no marker.
//
//Informational prints go to stdout only: zig 0.16's build_runner reports any step
//with nonempty stderr under a misleading 'failed command:' block even on success.
const std = @import("std");
const usage =
\\Usage: ./stagetree -key <pin|-> -src <path> -dest <path>
;
const marker_name = ".stage-materialized";
pub fn main(init: std.process.Init.Minimal) !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
var threaded: std.Io.Threaded = .init(arena, .{
.environ = init.environ,
.argv0 = .init(init.args),
});
defer threaded.deinit();
const io = threaded.io();
const args = try init.args.toSlice(arena);
var opt_key: ?[]const u8 = null;
var opt_src: ?[]const u8 = null;
var opt_dest: ?[]const u8 = null;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "-help", arg)) {
std.debug.print("{s}\n", .{usage});
return std.process.cleanExit(io);
} else if (std.mem.eql(u8, "-key", arg)) {
i += 1;
if (i >= args.len) fatal("expected arg after '{s}'", .{arg});
opt_key = args[i];
} else if (std.mem.eql(u8, "-src", arg)) {
i += 1;
if (i >= args.len) fatal("expected arg after '{s}'", .{arg});
opt_src = args[i];
} else if (std.mem.eql(u8, "-dest", arg)) {
i += 1;
if (i >= args.len) fatal("expected arg after '{s}'", .{arg});
opt_dest = args[i];
} else {
fatal("unknown argument '{s}'\n{s}", .{ arg, usage });
}
}
}
const key = opt_key orelse fatal("missing -key", .{});
const src = opt_src orelse fatal("missing -src", .{});
const dest = opt_dest orelse fatal("missing -dest", .{});
const cwd = std.Io.Dir.cwd();
//never touch a fossil/git checkout at the destination
inline for (.{ ".fslckout", "_FOSSIL_", ".git" }) |vcsmark| {
const p = try std.fs.path.join(arena, &.{ dest, vcsmark });
if (pathExists(cwd, io, p)) {
try say(io, arena, "stagetree: {s} is a VCS checkout ({s} present) - left untouched\n", .{ dest, vcsmark });
return std.process.cleanExit(io);
}
}
const src_stat = cwd.statFile(io, src, .{}) catch |err| {
fatal("cannot stat -src '{s}': {s}", .{ src, @errorName(err) });
};
const marker_path = try std.fs.path.join(arena, &.{ dest, marker_name });
const pinned = !std.mem.eql(u8, key, "-");
if (pinned and src_stat.kind == .directory) {
if (cwd.readFileAlloc(io, marker_path, arena, .limited(64 * 1024))) |existing| {
if (std.mem.eql(u8, std.mem.trim(u8, existing, " \r\n"), key)) {
try say(io, arena, "stagetree: {s} up to date (pin match)\n", .{dest});
return std.process.cleanExit(io);
}
} else |_| {}
}
if (src_stat.kind == .file) {
//single recipe file: parent dir must exist or be creatable; plain replace
if (std.fs.path.dirname(dest)) |destparent| {
cwd.createDirPath(io, destparent) catch |err| {
fatal("cannot create '{s}': {s}", .{ destparent, @errorName(err) });
};
}
cwd.deleteFile(io, dest) catch |err| switch (err) {
error.FileNotFound => {},
else => fatal("cannot remove stale '{s}': {s}", .{ dest, @errorName(err) }),
};
cwd.copyFile(src, cwd, dest, io, .{}) catch |err| {
fatal("copy '{s}' -> '{s}' failed: {s}", .{ src, dest, @errorName(err) });
};
try say(io, arena, "stagetree: staged file {s}\n", .{dest});
return std.process.cleanExit(io);
}
//directory tree: delete stale destination entirely (orphan files from a previous
//pin must not survive), then copy
cwd.deleteTree(io, dest) catch |err| {
fatal("cannot remove stale tree '{s}': {s}", .{ dest, @errorName(err) });
};
cwd.createDirPath(io, dest) catch |err| {
fatal("cannot create '{s}': {s}", .{ dest, @errorName(err) });
};
var src_dir = cwd.openDir(io, src, .{ .iterate = true }) catch |err| {
fatal("cannot open -src dir '{s}': {s}", .{ src, @errorName(err) });
};
defer src_dir.close(io);
var dest_dir = cwd.openDir(io, dest, .{}) catch |err| {
fatal("cannot open -dest dir '{s}': {s}", .{ dest, @errorName(err) });
};
defer dest_dir.close(io);
var nfiles: usize = 0;
var walker = try src_dir.walk(arena);
defer walker.deinit();
while (try walker.next(io)) |entry| {
switch (entry.kind) {
.directory => {
dest_dir.createDirPath(io, entry.path) catch |err| {
fatal("mkdir '{s}' in '{s}': {s}", .{ entry.path, dest, @errorName(err) });
};
},
.file => {
src_dir.copyFile(entry.path, dest_dir, entry.path, io, .{}) catch |err| {
fatal("copy '{s}' into '{s}' failed: {s}", .{ entry.path, dest, @errorName(err) });
};
nfiles += 1;
},
else => {},
}
}
if (pinned) {
try cwd.writeFile(io, .{ .sub_path = marker_path, .data = key });
}
try say(io, arena, "stagetree: staged {d} files -> {s}\n", .{ nfiles, dest });
return std.process.cleanExit(io);
}
fn say(io: std.Io, arena: std.mem.Allocator, comptime fmt: []const u8, args: anytype) !void {
const msg = try std.fmt.allocPrint(arena, fmt, args);
var f = std.Io.File.stdout();
try f.writeStreamingAll(io, msg);
}
fn pathExists(dir: std.Io.Dir, io: std.Io, p: []const u8) bool {
dir.access(io, p, .{}) catch return false;
return true;
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}

26
src/buildsuites/suite_tcl86/tools/suite_smoke.tcl

@ -0,0 +1,26 @@
#suite_smoke.tcl (G-099; forked from suite_tcl90's): smoke assertions run under
#a suite-built shell itself (the shell being smoked executes this script).
#
#args: -mode installed -expect <patchlevel>
# installed: interpreter reports the expected patchlevel AND resolves the
# INSTALLED on-disk library hermetically (the caller scrubs
# TCLLIBPATH/TCL_LIBRARY): reaching this script at all proves
# init.tcl resolved exe/dll-relative; tzdata reachable (clock
# format with a zone name) and auto_load present prove the tree.
# (8.6 has no zipfs - the 9.0 suite's 'zip' attached-library mode
# does not apply; the on-disk tree IS the deliverable.)
proc fail {msg} {puts stderr "suite_smoke FAIL: $msg"; flush stderr; exit 1}
array set opt {-mode installed -expect {}}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k'"}
set opt($k) $v
}
if {$opt(-expect) eq ""} {fail "missing -expect <patchlevel>"}
set pl [info patchlevel]
if {$pl ne $opt(-expect)} {fail "patchlevel mismatch: got '$pl' want '$opt(-expect)' ([info nameofexecutable])"}
if {[catch {clock format 0 -gmt 0 -format %Z} tz]} {fail "tzdata not reachable from installed library: $tz"}
if {[llength [info procs auto_load]] != 1} {fail "auto_load missing - installed library init incomplete"}
puts "suite_smoke OK: $pl (library [info library], tz=$tz) [info nameofexecutable]"
exit 0

218
src/buildsuites/suite_tcl86/tools/test_gate.tcl

@ -0,0 +1,218 @@
#test_gate.tcl (G-102 core gate; generalized for the G-107 library test runs):
#run a tcltest-driven testsuite under THIS interpreter's own shell
#([info nameofexecutable] - the suite-built tclsh when driven as a
#'zig build test-*' step) and act on the PARSED totals: tcltest drivers' exit
#codes do NOT reflect failures, so results are judged from the captured log.
#
#Modes (-mode, default gate):
# gate every failed test must appear in the tracked dispositioned baseline
# or the gate fails listing the unexplained names (the core test-gate
# mechanism, G-098); notes baseline entries that no longer fail.
# record the run must COMPLETE (totals parsed) but failures do not fail the
# step - results are recorded as evidence (G-107 library policy tier
# for suites without a dispositioned baseline yet).
#EVERY mode: a run with no parseable aggregate totals line FAILS the step -
#infrastructure breakage must never masquerade as recorded results.
#
#Driver: <testsdir>/all.tcl by default. tcllib-family trees are driven via
#their own support/devel/all.tcl (-driver), which runs every module's test
#files in per-file child interps and aggregates counts into the same standard
#totals line ('all.tcl: Total N Passed N Skipped N Failed N'). That driver
#marks a test file that errors out with '@+'/'@|' errorInfo blocks - counted
#here as errored_files evidence (recorded in the summary, not a gate criterion;
#the gate criterion stays totals-vs-baseline, core-gate semantics). The
#runAllTests-vintage drivers (tcl core, tk) report 'Test files exiting with
#errors:' - captured as errorexit_files evidence the same way.
#
#Evidence summary (-summaryfile): a line-record file for artifact-metadata
#consumers (G-103). Each non-comment line is a Tcl list: '<key> <value>'
#(value may itself be a list). Shape documented in
#goals/G-107-buildsuite-library-tests.md.
#
#args: -testsdir <dir> -logfile <file> ?-mode gate|record?
# ?-baseline <file>? (required in gate mode)
# ?-driver <file>? (default all.tcl; relative paths resolve against -testsdir)
# ?-summaryfile <file>? ?-library <name>? ?-testargs {tcltest args}?
# ?-notfiles {file-tail-glob ...}?
#
#-notfiles is a Tcl list of test-file glob patterns excluded from the run
#(driver -notfile). It exists because -testargs cannot carry a multi-file
#exclusion through the option chain (-notfile takes one list value; a flat
#-testargs re-split on whitespace would break it). Use it to disposition
#test FILES that cannot run in this context - e.g. Tk's native-dialog suites
#(winDialog.test, winMsgbox.test) whose message-injection automation does not
#drive the real OS dialogs under the batch/suite-built shell, so each of their
#~90 cases blocks waiting on a human. Recorded in the evidence summary.
proc fail {msg} {puts stderr "test_gate ERROR: $msg"; flush stderr; exit 1}
array set opt {-testsdir {} -baseline {} -logfile {} -testargs {} -mode gate -driver all.tcl -summaryfile {} -library {} -notfiles {}}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k' (expected -testsdir -baseline -logfile -mode -driver -summaryfile -library -testargs -notfiles)"}
set opt($k) $v
}
foreach req {-testsdir -logfile} {
if {$opt($req) eq ""} {fail "missing required option $req"}
}
if {$opt(-mode) ni {gate record}} {fail "unknown -mode '$opt(-mode)' (expected gate|record)"}
if {![file isdirectory $opt(-testsdir)]} {fail "no tests dir at $opt(-testsdir) - build first"}
if {$opt(-mode) eq "gate"} {
if {$opt(-baseline) eq ""} {fail "-mode gate requires -baseline"}
if {![file exists $opt(-baseline)]} {fail "no baseline file at $opt(-baseline) - for a first census run the step in record mode (-Dtestpolicy-<lib>=record), disposition the recorded failures into the baseline, then gate"}
}
set library $opt(-library)
if {$library eq ""} {set library [file tail [file normalize $opt(-testsdir)]]}
set exe [info nameofexecutable]
set logfile [file normalize $opt(-logfile)]
file mkdir [file dirname $logfile]
set driver $opt(-driver)
if {[file pathtype $driver] ne "absolute"} {set driver [file join $opt(-testsdir) $driver]}
set driver [file normalize $driver]
if {![file exists $driver]} {fail "no driver script at $driver"}
#-- evidence summary (written on every outcome once the run has been attempted)
set summary [dict create]
proc sumset {key value} {global summary; dict set summary $key $value}
proc write_summary {} {
global opt summary
if {$opt(-summaryfile) eq ""} return
set sf [file normalize $opt(-summaryfile)]
file mkdir [file dirname $sf]
set f [open $sf w]
fconfigure $f -translation lf
puts $f "# library-test evidence summary (G-107; consumed by G-103 artifact metadata)"
puts $f "# line records: <key> <value> - parse each non-comment line as a Tcl list."
puts $f "# shape: goals/G-107-buildsuite-library-tests.md (punkshell repo)"
dict for {k v} $summary {puts $f [list $k $v]}
close $f
}
proc finish {result failreason msg} {
#single exit point after the run: record outcome, write summary, report
global opt logfile
sumset result $result
if {$failreason ne ""} {sumset failreason $failreason}
write_summary
if {$opt(-summaryfile) ne ""} {
puts "test_gate: evidence: [file normalize $opt(-summaryfile)] (log: $logfile)"
}
if {$result eq "pass"} {
puts "test_gate: $msg"
exit 0
}
puts stderr "test_gate $msg"
flush stderr
exit 1
}
sumset library $library
sumset mode $opt(-mode)
sumset shell $exe
sumset driver $driver
sumset testsdir [file normalize $opt(-testsdir)]
sumset testargs $opt(-testargs)
sumset notfiles $opt(-notfiles)
sumset baseline [expr {$opt(-baseline) ne "" ? [file normalize $opt(-baseline)] : ""}]
sumset logfile $logfile
#-notfiles is passed as ONE driver arg (a Tcl list value tcltest reads as a
#list of -notfile patterns) - it must NOT be whitespace-split like -testargs.
set exclargs {}
if {[llength $opt(-notfiles)]} {lappend exclargs -notfile $opt(-notfiles)}
puts "test_gate: $library testsuite under $exe [expr {[llength $opt(-testargs)] ? $opt(-testargs) : "(full suite)"}] (mode $opt(-mode))[expr {[llength $opt(-notfiles)] ? " excluding files: $opt(-notfiles)" : ""}] -> $logfile"
flush stdout
set savedpwd [pwd]
cd $opt(-testsdir)
catch {exec $exe $driver {*}$opt(-testargs) {*}$exclargs > $logfile 2>@1}
cd $savedpwd
set f [open $logfile r]; set testout [read $f]; close $f
#aggregate totals: the master driver's final cleanupTests/runAllTests line.
#Take the LAST match - per-file summary lines carry the file's own name, but a
#driver arrangement that reuses 'all.tcl' for a nested stage must not shadow
#the final aggregate.
set quads [regexp -all -inline {all\.tcl:\s+Total\s+(\d+)\s+Passed\s+(\d+)\s+Skipped\s+(\d+)\s+Failed\s+(\d+)} $testout]
if {![llength $quads]} {
finish fail no-totals "FAIL ($library): no all.tcl totals line found in $logfile - run did not complete"
}
lassign [lrange $quads end-3 end] ntotal npassed nskipped nfailed
puts "test_gate: totals: $ntotal run, $npassed passed, $nskipped skipped, $nfailed failed"
sumset total $ntotal
sumset passed $npassed
sumset skipped $nskipped
sumset failed $nfailed
set failednames {}
foreach {- tname} [regexp -all -inline -line {^==== (\S+) FAILED} $testout] {
if {$tname ni $failednames} {lappend failednames $tname}
}
sumset failednames $failednames
#file-level completeness evidence (recorded, not gate criteria - the gate
#criterion is totals-vs-baseline, matching the core gate):
#tcllib-family driver: a test file that errors mid-source is caught and marked
#with an '@+' line followed by '@|' errorInfo lines.
set errored_count 0
foreach line [split $testout \n] {
if {$line eq "@+"} {incr errored_count}
}
sumset errored_files $errored_count
#runAllTests-vintage drivers (tcl core, tk): 'Test files exiting with errors:'
#section lists files whose child run errored.
set errorexit_files {}
if {[regexp -indices {Test files exiting with errors:} $testout headidx]} {
set tailtext [string range $testout [lindex $headidx 1]+1 end]
foreach line [split $tailtext \n] {
set line [string trim $line]
if {$line eq ""} continue
if {[regexp {^(\S+\.test)$} $line -> fname]} {
lappend errorexit_files $fname
} else {
break
}
}
}
sumset errorexit_files $errorexit_files
if {$errored_count || [llength $errorexit_files]} {
puts "test_gate: note: file-level errors recorded: errored_files=$errored_count errorexit_files=[llength $errorexit_files] ([join $errorexit_files { }])"
}
if {$opt(-mode) eq "record"} {
finish pass "" "RECORDED ($library): $ntotal run, $nfailed failed ([llength $failednames] distinct names) - evidence artifacts written"
}
#-- gate mode: diff failed names against the dispositioned baseline
#baseline: one test name per line; '#' comments carry the disposition reasons
set baseline {}
set f [open $opt(-baseline) r]
foreach line [split [read $f] \n] {
set line [string trim $line]
if {$line eq "" || [string index $line 0] eq "#"} continue
lappend baseline [lindex $line 0]
}
close $f
set unexplained {}
foreach tname $failednames {
if {$tname ni $baseline} {lappend unexplained $tname}
}
sumset unexplained $unexplained
set stale {}
if {![llength $opt(-testargs)]} {
#only meaningful for full runs: baseline entries that no longer fail
foreach tname $baseline {
if {$tname ni $failednames} {lappend stale $tname}
}
if {[llength $stale]} {
puts "test_gate: note: [llength $stale] baseline entries did not fail this run (candidates for removal): $stale"
}
}
sumset stalebaseline $stale
if {[llength $unexplained]} {
puts stderr "test_gate GATE FAIL ($library): [llength $unexplained] failed test(s) not in the expected-failure baseline:"
foreach tname $unexplained {puts stderr " $tname"}
puts stderr "(full output: $logfile; baseline: $opt(-baseline))"
flush stderr
finish fail unexplained "GATE FAIL ($library): [llength $unexplained] unexplained failure(s) - see above"
}
finish pass "" "GATE PASS ($library): [llength $failednames] failures, all baselined"

92
src/buildsuites/suite_tcl86/tools/wrapfiletofile.zig

@ -0,0 +1,92 @@
const std = @import("std");
const usage =
\\Usage: ./wrapfiletofile [options]
\\
\\Options:
\\ -prefix STRING
\\ -prefixnl BOOLEAN
\\ -input INPUTFILENAME
\\ -outut OUTPUTFILENAME
;
pub fn main(init: std.process.Init.Minimal) !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
var threaded: std.Io.Threaded = .init(arena, .{
.environ = init.environ,
.argv0 = .init(init.args),
});
defer threaded.deinit();
const io = threaded.io();
const args = try init.args.toSlice(arena);
var opt_input_file_path: ?[]const u8 = null;
var opt_output_file_path: ?[]const u8 = null;
var opt_prefix: ?[]const u8 = null;
var opt_prefixnl: bool = true;
{
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "-help", arg)) {
std.debug.print("{s}\n", .{usage});
return std.process.cleanExit(io);
} else if (std.mem.eql(u8, "-prefix", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
opt_prefix = args[i];
} else if (std.mem.eql(u8, "-input", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_input_file_path = args[i];
} else if (std.mem.eql(u8, "-output", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
opt_output_file_path = args[i];
} else if (std.mem.eql(u8, "-prefixnl", arg)) {
i += 1;
if (i > args.len) fatal("expected arg after '{s}'", .{arg});
if (std.mem.eql(u8, "1", args[i])) {
opt_prefixnl = true;
} else {
opt_prefixnl = false;
}
}
}
}
const input_file_path = opt_input_file_path orelse fatal("missing -input file", .{});
const output_file_path = opt_output_file_path orelse fatal("missing -output file", .{});
const prefix = opt_prefix orelse fatal("missing -prefix string", .{});
const input_data = std.Io.Dir.cwd().readFileAlloc(io, input_file_path, arena, .unlimited) catch |err| {
fatal("Unable to read '{s}': {s}", .{ input_file_path, @errorName(err) });
};
defer arena.free(input_data);
//no informational print here: std.debug.print writes to stderr, and zig 0.16's
//build_runner reports any step with nonempty stderr under a misleading
//"failed command:" block even on success. Quiet on success, loud on error.
var output_file = std.Io.Dir.cwd().createFile(io, output_file_path, .{}) catch |err| {
fatal("Unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
};
defer output_file.close(io);
try output_file.writeStreamingAll(io, prefix);
if (opt_prefixnl) {
try output_file.writeStreamingAll(io, "\n");
}
try output_file.writeStreamingAll(io, input_data);
return std.process.cleanExit(io);
}
fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.debug.print(format, args);
std.process.exit(1);
}
Loading…
Cancel
Save