From a20a64a2a1aaf2752e94f771b6ff796fe5d20c3a Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 26 Jul 2026 05:00:06 +1000 Subject: [PATCH] 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 --- ARCHITECTURE.md | 4 +- GOALS-archive.md | 4 + GOALS.md | 4 - goals/G-005-zig-build-infrastructure.md | 2 +- goals/G-100-suite-tcl86-tk-tcllib.md | 17 +- goals/G-101-tcl86-kit-container-strategy.md | 4 +- .../G-099-suite-tcl86-buildsuite.md | 76 +- src/buildsuites/AGENTS.md | 8 +- src/buildsuites/suite_tcl86/README.md | 60 +- src/buildsuites/suite_tcl86/build.zig | 7 + src/buildsuites/suite_tcl86/build86.zig | 915 +++++++++++++++++- src/buildsuites/suite_tcl86/build_common.zig | 141 +++ .../build_tclthread86/build_tclthread86.zig | 137 +++ .../build_tclvfs86/build_tclvfs86.zig | 161 +++ .../suite_tcl86/build_zlib86/build_zlib86.zig | 50 + .../suite_tcl86/expected_test_failures.txt | 23 + .../expected_test_failures_tclvfs.txt | 26 + .../expected_test_failures_thread.txt | 13 + src/buildsuites/suite_tcl86/sources.config | 8 +- src/buildsuites/suite_tcl86/src/main.zig | 4 + src/buildsuites/suite_tcl86/suite.tcl | 22 +- .../suite_tcl86/tools/pkg_smoke.tcl | 42 + .../suite_tcl86/tools/stagetree.zig | 173 ++++ .../suite_tcl86/tools/suite_smoke.tcl | 26 + .../suite_tcl86/tools/test_gate.tcl | 218 +++++ .../suite_tcl86/tools/wrapfiletofile.zig | 92 ++ 26 files changed, 2166 insertions(+), 71 deletions(-) rename goals/{ => archive}/G-099-suite-tcl86-buildsuite.md (66%) create mode 100644 src/buildsuites/suite_tcl86/build.zig create mode 100644 src/buildsuites/suite_tcl86/build_common.zig create mode 100644 src/buildsuites/suite_tcl86/build_tclthread86/build_tclthread86.zig create mode 100644 src/buildsuites/suite_tcl86/build_tclvfs86/build_tclvfs86.zig create mode 100644 src/buildsuites/suite_tcl86/build_zlib86/build_zlib86.zig create mode 100644 src/buildsuites/suite_tcl86/expected_test_failures.txt create mode 100644 src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt create mode 100644 src/buildsuites/suite_tcl86/expected_test_failures_thread.txt create mode 100644 src/buildsuites/suite_tcl86/src/main.zig create mode 100644 src/buildsuites/suite_tcl86/tools/pkg_smoke.tcl create mode 100644 src/buildsuites/suite_tcl86/tools/stagetree.zig create mode 100644 src/buildsuites/suite_tcl86/tools/suite_smoke.tcl create mode 100644 src/buildsuites/suite_tcl86/tools/test_gate.tcl create mode 100644 src/buildsuites/suite_tcl86/tools/wrapfiletofile.zig diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 687b4810..fa81e6fe 100644 --- a/ARCHITECTURE.md +++ b/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 diff --git a/GOALS-archive.md b/GOALS-archive.md index 31567fc9..8e549928 100644 --- a/GOALS-archive.md +++ b/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 ` filters to the named entries; `make.tcl bake ` 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. diff --git a/GOALS.md b/GOALS.md index d20d5d23..15b26988 100644 --- a/GOALS.md +++ b/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 diff --git a/goals/G-005-zig-build-infrastructure.md b/goals/G-005-zig-build-infrastructure.md index 8d7cab57..b32f4091 100644 --- a/goals/G-005-zig-build-infrastructure.md +++ b/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 diff --git a/goals/G-100-suite-tcl86-tk-tcllib.md b/goals/G-100-suite-tcl86-tk-tcllib.md index fbbb39a7..f0ff496b 100644 --- a/goals/G-100-suite-tcl86-tk-tcllib.md +++ b/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). diff --git a/goals/G-101-tcl86-kit-container-strategy.md b/goals/G-101-tcl86-kit-container-strategy.md index 02d43c20..9dceefae 100644 --- a/goals/G-101-tcl86-kit-container-strategy.md +++ b/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 diff --git a/goals/G-099-suite-tcl86-buildsuite.md b/goals/archive/G-099-suite-tcl86-buildsuite.md similarity index 66% rename from goals/G-099-suite-tcl86-buildsuite.md rename to goals/archive/G-099-suite-tcl86-buildsuite.md index 617159f8..f7a299fa 100644 --- a/goals/G-099-suite-tcl86-buildsuite.md +++ b/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 + vfst 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 diff --git a/src/buildsuites/AGENTS.md b/src/buildsuites/AGENTS.md index 5423959e..f3bb8d08 100644 --- a/src/buildsuites/AGENTS.md +++ b/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) diff --git a/src/buildsuites/suite_tcl86/README.md b/src/buildsuites/suite_tcl86/README.md index ba4bb7d8..6ac816d7 100644 --- a/src/buildsuites/suite_tcl86/README.md +++ b/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 diff --git a/src/buildsuites/suite_tcl86/build.zig b/src/buildsuites/suite_tcl86/build.zig new file mode 100644 index 00000000..b0d52194 --- /dev/null +++ b/src/buildsuites/suite_tcl86/build.zig @@ -0,0 +1,7 @@ +//build.zig - default-entry shim so plain 'zig build ' 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; diff --git a/src/buildsuites/suite_tcl86/build86.zig b/src/buildsuites/suite_tcl86/build86.zig index b6a6c4b4..193da51a 100644 --- a/src/buildsuites/suite_tcl86/build86.zig +++ b/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//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// 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-=gate|record|skip -Dtestargs-="..." + // -Dtestnotfiles-="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/ (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 -> /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); } diff --git a/src/buildsuites/suite_tcl86/build_common.zig b/src/buildsuites/suite_tcl86/build_common.zig new file mode 100644 index 00000000..00ffe985 --- /dev/null +++ b/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 ""' 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 ...' 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: +//.., 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"); +} diff --git a/src/buildsuites/suite_tcl86/build_tclthread86/build_tclthread86.zig b/src/buildsuites/suite_tcl86/build_tclthread86/build_tclthread86.zig new file mode 100644 index 00000000..a39f11da --- /dev/null +++ b/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: threadt +// (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/, 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/{, 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 }; +} diff --git a/src/buildsuites/suite_tcl86/build_tclvfs86/build_tclvfs86.zig b/src/buildsuites/suite_tcl86/build_tclvfs86/build_tclvfs86.zig new file mode 100644 index 00000000..87c8dbde --- /dev/null +++ b/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 (vfst.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 ); 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 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) 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 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 + \\#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; +} diff --git a/src/buildsuites/suite_tcl86/build_zlib86/build_zlib86.zig b/src/buildsuites/suite_tcl86/build_zlib86/build_zlib86.zig new file mode 100644 index 00000000..aeed5872 --- /dev/null +++ b/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; +} diff --git a/src/buildsuites/suite_tcl86/expected_test_failures.txt b/src/buildsuites/suite_tcl86/expected_test_failures.txt new file mode 100644 index 00000000..b71630f9 --- /dev/null +++ b/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 +# ///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 diff --git a/src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt b/src/buildsuites/suite_tcl86/expected_test_failures_tclvfs.txt new file mode 100644 index 00000000..1e834aa9 --- /dev/null +++ b/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 diff --git a/src/buildsuites/suite_tcl86/expected_test_failures_thread.txt b/src/buildsuites/suite_tcl86/expected_test_failures_thread.txt new file mode 100644 index 00000000..bb260bc7 --- /dev/null +++ b/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) diff --git a/src/buildsuites/suite_tcl86/sources.config b/src/buildsuites/suite_tcl86/sources.config index 5fd461cb..8d550191 100644 --- a/src/buildsuites/suite_tcl86/sources.config +++ b/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 diff --git a/src/buildsuites/suite_tcl86/src/main.zig b/src/buildsuites/suite_tcl86/src/main.zig new file mode 100644 index 00000000..37d96762 --- /dev/null +++ b/src/buildsuites/suite_tcl86/src/main.zig @@ -0,0 +1,4 @@ +const std = @import("std"); + +pub const _start = void; +pub const WinMainCRTStartup = void; diff --git a/src/buildsuites/suite_tcl86/suite.tcl b/src/buildsuites/suite_tcl86/suite.tcl index 5c94e8c3..ba3cacd3 100644 --- a/src/buildsuites/suite_tcl86/suite.tcl +++ b/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 fossil clone store (default ~/.punkshell/fossils; env PUNK_FOSSIL_STORE) # -seedfossils read-only seed collection for missing clones (default ~/.fossils; "" disables) # -all <0|1> with 'clean': reserved (the clone store lives outside the stage) -# -steps zig build steps. 'build' default: {install} (grows with the -# recipe: thread/tclvfs installs, smoke, per G-099). +# -steps 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 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 } diff --git a/src/buildsuites/suite_tcl86/tools/pkg_smoke.tcl b/src/buildsuites/suite_tcl86/tools/pkg_smoke.tcl new file mode 100644 index 00000000..05f9475e --- /dev/null +++ b/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 additionally asserts the package's +#critcl accelerator engaged (::::accel(critcl) == 1, tcllibc proof). +#Formerly suite.tcl inline smokes. +# +#args: ?-gui 0|1? ?-accel ? 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 diff --git a/src/buildsuites/suite_tcl86/tools/stagetree.zig b/src/buildsuites/suite_tcl86/tools/stagetree.zig new file mode 100644 index 00000000..0ee7aba9 --- /dev/null +++ b/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/ 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 -src -dest +// +// -key pinned-tree mode: if /.stage-materialized already contains +// exactly , 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 -src -dest +; + +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); +} diff --git a/src/buildsuites/suite_tcl86/tools/suite_smoke.tcl b/src/buildsuites/suite_tcl86/tools/suite_smoke.tcl new file mode 100644 index 00000000..7b023158 --- /dev/null +++ b/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 +# 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 "} +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 diff --git a/src/buildsuites/suite_tcl86/tools/test_gate.tcl b/src/buildsuites/suite_tcl86/tools/test_gate.tcl new file mode 100644 index 00000000..3cd56df6 --- /dev/null +++ b/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: /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: ' ' +#(value may itself be a list). Shape documented in +#goals/G-107-buildsuite-library-tests.md. +# +#args: -testsdir -logfile ?-mode gate|record? +# ?-baseline ? (required in gate mode) +# ?-driver ? (default all.tcl; relative paths resolve against -testsdir) +# ?-summaryfile ? ?-library ? ?-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-=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: - 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" diff --git a/src/buildsuites/suite_tcl86/tools/wrapfiletofile.zig b/src/buildsuites/suite_tcl86/tools/wrapfiletofile.zig new file mode 100644 index 00000000..15211f90 --- /dev/null +++ b/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); +}