Browse Source

G-102 M3+M4: post-tclsh phases as recipe steps; suite.tcl reduced to dev wrapper

The whole pipeline now lives in the zig recipe; Tcl remains the tool exactly
where a tclsh is guaranteed to exist - run by the FRESHLY BUILT suite shell
as build steps:

- New steps (native-host builds): smoke (static shell patchlevel; zip shell
  patchlevel + attached tzdata/autoload), tklib / tcllib (their own installers,
  batch, -pkg-path as a cached output dir installed into the prefix, + package
  smokes incl withdrawn-Tk tooltip), tcllibc (critcl driving zig cc via the
  tracked critcl_zig.config, module list parsed from tcllib's version.tcl,
  pinned-zig dir prepended to PATH, + accel smoke), test-gate (tools/
  test_gate.tcl - the parsed-totals gate vs expected_test_failures.txt,
  -Dtestargs passthrough).
- Package landing moves into the recipe proper: thread pkgIndex.tcl
  (build_tclthread), tk pkgIndex.tcl (build_tk), and the tclvfs script package
  incl .in-template substitution + dll (build_tclvfs_shared
  install_tclvfs_package) - all hooked to the install step.
- Step scripts: tools/test_gate.tcl, tools/suite_smoke.tcl, tools/pkg_smoke.tcl
  (run under the built shell; assertions live in Tcl).
- Two windows-childproofing findings encoded: built-shell Runs execute the
  CACHED artifact (no ../lib/tcl9.0 beside it) so each such Run gets
  TCL_LIBRARY pointed at the source tree's script library (the zip smoke
  deliberately excluded - its attached library is the thing under test); and
  cached (non-side-effect) Runs spawn with stdout IGNORED, leaving the tcl
  child without a stdout channel ('can not find channel named "stdout"',
  critcl's logging) - such runs capture stdout to give the child a real pipe
  while keeping caching.
- suite.tcl: thinned to the fossil dev flow - fetch live checkouts (git kind
  now handled in the main loop for critcl's pin), stage the recipe, delegate
  to the staged steps (run_zig_steps; 'test' delegates to test-gate). Default
  -steps and the bootstrap default grow to the full pipeline short of
  test-gate. critcl_zig.config + expected_test_failures.txt join both staging
  lists. Docs updated (README two-flow section, sources.config two-surface
  header, overlay section); .gitignore learns .zig-cache/ and zig-pkg/ (zig
  0.16 extracts fetched packages into zig-pkg/ beside the manifest).

Verified (fossil flow): full pipeline PASS - suite_smoke static+zip, tklib
tooltip:2.0.4 under withdrawn Tk, tcllib md5/sha1/cmdline, tcllibc:2.0 with
md5-critcl-accel:1; 'suite.tcl test -testargs {-file http.test}' delegates to
the gate step (528/519/9/0 GATE PASS).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
e5db051389
  1. 2
      .gitignore
  2. 83
      src/buildsuites/suite_tcl90/README.md
  3. 174
      src/buildsuites/suite_tcl90/build905.zig
  4. 54
      src/buildsuites/suite_tcl90/build_common.zig
  5. 12
      src/buildsuites/suite_tcl90/build_tclthread/build_tclthread.zig
  6. 39
      src/buildsuites/suite_tcl90/build_tclvfs/build_tclvfs_shared.zig
  7. 15
      src/buildsuites/suite_tcl90/build_tk/build_tk.zig
  8. 12
      src/buildsuites/suite_tcl90/sources.config
  9. 353
      src/buildsuites/suite_tcl90/suite.tcl
  10. 42
      src/buildsuites/suite_tcl90/tools/pkg_smoke.tcl
  11. 26
      src/buildsuites/suite_tcl90/tools/suite_smoke.tcl
  12. 71
      src/buildsuites/suite_tcl90/tools/test_gate.tcl

2
.gitignore vendored

@ -81,7 +81,9 @@ _FOSSIL_
todo.txt
zig-cache/
.zig-cache/
zig-out/
zig-pkg/
/release/
/debug/
/build/

83
src/buildsuites/suite_tcl90/README.md

@ -1,24 +1,51 @@
# suite_tcl90 - zig build of the Tcl 9.0 windows runtime (G-096)
# suite_tcl90 - zig build of the Tcl 9.0 windows runtime (G-096, G-102)
The first real, tracked buildsuite: reproducibly builds `tclsh90s.exe` (static) and
`tclsh90szip.exe` (self-contained zipfs) plus the thread/vfs extension dlls from
a fresh Tcl core-9-0-branch checkout, using zig only (no MS toolchain, per the project
`tclsh90szip.exe` (self-contained zipfs) plus the TCLSH_PIPEREPL variants and the
thread/vfs/tk extension dlls, tklib/tcllib installs and critcl-built tcllibc, from
Tcl core-9-0-branch sources, using zig only (no MS toolchain, per the project
toolchain policy). (vqtcl/vlerq was removed 2026-07-20 - an old-tclkit experiment;
future tclkit investigation has TEMP_REFERENCE/metakit and TEMP_REFERENCE/KitCreator
as reference material.)
Everything transient lives under `src/buildsuites/_build/suite_tcl90/` (VCS-ignored via
the repo's existing `_build` globs): staged recipe, source checkouts, zig cache, and the
`out/` install prefix. The tracked content here is the recipe and its driver.
the repo's existing `_build` globs): staged recipe, source trees, zig cache, and the
`out/` install prefix. The tracked content here is the recipe and its drivers.
## Usage
## Two flows, one pipeline (G-102)
The PIPELINE - configure-product generation (overlays, source trees never written),
compile, package installs, smokes, test gate - lives entirely in the zig recipe as
build steps. Two ways to arrange sources and drive it:
**No-tclsh bootstrap flow** (pinned sources; nothing but the pinned zig required):
zig build stage # fetch build.zig.zon-pinned sources + stage recipe -> ../_build/suite_tcl90
zig build bootstrap # stage + run the staged pipeline end-to-end
run from THIS directory with the pinned zig. Sources come from `build.zig.zon` -
content-hashed per-checkin tarball pins ('refresh' = bump a pin; `zig fetch
--save=<name> <url>` edits the manifest). Steps for the nested staged build:
`-Dsteps=...` (repeat the flag), default install install-libraries make-zipfs smoke
tklib tcllib tcllibc. The test gate: `zig build bootstrap -Dsteps=test-gate` (or
invoke the staged build file directly, as suite.tcl does). zig extracts fetched
packages into `zig-pkg/` beside the manifest and `.zig-cache/` for the outer
invocation - both VCS-ignored.
**Fossil dev flow** (live checkouts; a plain tclsh drives fetch+staging):
tclsh suite.tcl build ?options?
tclsh suite.tcl test ?-testargs {...}?
tclsh suite.tcl clean
`suite.tcl` arranges LIVE fossil/git checkouts per `sources.config` (branch work,
`-refresh`, `-tclbranch` overrides) into the same stage layout, then delegates to
the same staged zig steps. An existing fossil/git checkout in the stage is never
touched by the bootstrap staging (guarded twice), so the flows can be mixed.
See `suite.tcl`'s header for options (`-zig`, `-optimize`, `-tclbranch`, `-refresh`,
`-steps`). Zig resolution: `-zig` > `$env(PUNK_ZIG)` > the pinned
toolchain at `bin/tools/zig-x86_64-windows-0.14.1/zig.exe` > `zig` on PATH.
toolchain at `bin/tools/zig-x86_64-windows-0.16.0/zig.exe` > `zig` on PATH.
## Pinned zig
@ -51,12 +78,17 @@ producing stale/duplicate artifacts at link time).
cache-install artifact).
- `src/main.zig` - stub root module that disables zig's own start symbols so the mingw
CRT provides the exe entry.
- `tools/` - `wrapfiletofile.zig` (uuid-header generation used by prepare-source),
`zipfs_mkzip.tcl` / `zipfs_mkimg.tcl` (make-zipfs drivers).
- `tools/` - `wrapfiletofile.zig` (uuid-header overlay generation),
`stagetree.zig` (bootstrap-mode source/recipe materializer),
`zipfs_mkzip.tcl` / `zipfs_mkimg.tcl` (make-zipfs drivers), and the
suite-built-shell step scripts `test_gate.tcl` / `suite_smoke.tcl` /
`pkg_smoke.tcl` (G-102).
- `patches/` - recovered 2024 experiment patches, including TCLSH_PIPEREPL
(see `patches/README.md`). NOT applied by the default build - stock sources.
- `suite.tcl` - the driver: stage recipe -> arrange sources -> generate the
configure-produced files a fresh checkout lacks -> build -> smoke-test.
(see `patches/README.md`) - compiled into the SEPARATE `tclsh90spr`/
`tclsh90sprzip` products; the stock shells stay unpatched.
- `build.zig.zon` - pinned source manifest for the bootstrap flow.
- `suite.tcl` - the fossil dev driver: stage recipe -> arrange live checkouts ->
delegate to the staged recipe's steps.
## Sources
@ -94,16 +126,23 @@ changes additionally touch `build905.zig` and the driver's install steps
(checkout dir names, version-derived tm/dll/manifest names) - see the depth-limit
note in sources.config.
## Fresh-checkout preparations the driver performs
Normally products of `configure`/`nmake` that a bare checkout lacks (G-076 findings):
- `win/tclUuid.h` - generated from `manifest.uuid`.
- `win/tclsh.exe.manifest` - generated from its `.in` (`TCL_WIN_VERSION` per the tree's
own formula: `TCL_VERSION . releaselevel . patchlevel-stripped-of-'ab.'`).
- `thread/win/thread.rc` - gains `#include "../../tcl905/generic/tcl.h"` (the zig rc
compile does not receive the makefile's include paths; historically an uncommitted
tweak in the 2024 checkout, now a scripted idempotent edit of the staged copy).
## Fresh-checkout preparations (configure-products)
Normally products of `configure`/`nmake` that a bare checkout lacks (G-076 findings).
Since G-102 these are generated INSIDE the zig recipe as build-cache OVERLAYS -
source trees are never written (overlay include dirs added first so stale tree
copies can never shadow; rc files that reference generated resources compile from
overlay copies where rc-file-relative lookup would otherwise win):
- `tclUuid.h` / `tkUuid.h` / `threadUuid.h` - from each tree's `manifest.uuid`
(wrapfiletofile Run steps; consumed via overlay include dirs).
- `tclsh.exe.manifest` / `wish.exe.manifest` - from their `.in` templates
(`*_WIN_VERSION` per the trees' own formula:
`VERSION . releaselevel . patchlevel-stripped-of-'ab.'`).
- `thread.rc` - overlay copy gains `#include "tcl.h"` resolved via rc include
paths (the zig rc compile does not receive the makefile's include paths;
historically an uncommitted checkout tweak, later a scripted edit, now an
overlay - checkouts stay pristine).
## Lineage

174
src/buildsuites/suite_tcl90/build905.zig

@ -255,6 +255,12 @@ pub fn build(b: *std.Build) !void {
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");
//Runs of the built (non-zip) shell execute the CACHED artifact, which has no
//../lib/tcl9.0 beside it - point such runs at the source tree's own script
//library so init.tcl resolves (the zip shell's runs must NOT get this: its
//attached library is the thing under test).
const tcl_library_src = common.replaceAll(b, b.pathFromRoot(tcl_source_folder ++ "/library"), "\\", "/");
// ================================================
var ac_flags = std.array_list.Managed([]const u8).init(b.allocator);
@ -1759,6 +1765,11 @@ pub fn build(b: *std.Build) !void {
const build_tclvfs_step = b.step("build-tclvfs", "build tclvfs shared library");
build_tclvfs_step.dependOn(&tclvfs_compile.step);
tclsh_exe.step.dependOn(&tclvfs_compile.step); // review
if (target.result.os.tag == .windows) {
//G-102: vfs script package (lib/vfs1.4.2) installed by the recipe - the repl
//code interp wants vfs/vfs::zip (modpod mounting). Formerly suite.tcl post-build.
try tclvfs_shared.install_tclvfs_package("../tclvfs", b, tclvfs_compile);
}
////////////////////
//vqtcl/vlerq removed from the suite (user 2026-07-20): it was an attempt at old
@ -2071,6 +2082,8 @@ pub fn build(b: *std.Build) !void {
} else {
mkzip_run = b.addSystemCommand(&.{"tclsh90"});
}
common.scrubTclEnv(mkzip_run);
mkzip_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
mkzip_run.addFileArg(b.path("tools/zipfs_mkzip.tcl"));
mkzip_run.addArg("-outfile");
const out_mkzip = mkzip_run.addOutputFileArg("mkzipfs.zip");
@ -2097,6 +2110,8 @@ pub fn build(b: *std.Build) !void {
//require an existing tclsh90 - with zipfs support on the path for x-builds
mkimg_run = b.addSystemCommand(&.{"tclsh90"});
}
common.scrubTclEnv(mkimg_run);
mkimg_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
mkimg_run.addFileArg(b.path("tools/zipfs_mkimg.tcl"));
mkimg_run.addArg("-outfile");
var out_mkimg: std.Build.LazyPath = undefined;
@ -2136,6 +2151,8 @@ pub fn build(b: *std.Build) !void {
//require an existing tclsh90 - with zipfs support on the path for x-builds
mkimg_pr_run = b.addSystemCommand(&.{"tclsh90"});
}
common.scrubTclEnv(mkimg_pr_run);
mkimg_pr_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
mkimg_pr_run.addFileArg(b.path("tools/zipfs_mkimg.tcl"));
mkimg_pr_run.addArg("-outfile");
var out_mkimg_pr: std.Build.LazyPath = undefined;
@ -2178,6 +2195,159 @@ pub fn build(b: *std.Build) !void {
//install without zipfs
//b.getInstallStep().dependOn(install_libraries);
// ================== G-102 post-tclsh phases ==================
//Tcl phases executed by the FRESHLY BUILT suite shell as build steps - Tcl stays
//the tool exactly where a tclsh is guaranteed to exist because the suite just
//built it. Formerly suite.tcl's post-build blocks; suite.tcl now drives these
//via step names. Native-host builds only (the runs execute the built artifacts).
if (target.result.os.tag == builtin.os.tag) {
//-- smoke: shells respond with the expected patchlevel; the zip shell proves
//the attached library (tzdata + autoload)
const smoke_step = b.step("smoke", "smoke-test the built shells (patchlevel; zip: attached tzdata/autoload)");
const smoke_static = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(smoke_static);
smoke_static.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
smoke_static.has_side_effects = true;
smoke_static.addFileArg(b.path("tools/suite_smoke.tcl"));
smoke_static.addArgs(&.{ "-mode", "static", "-expect", tcl_h_patchlevel });
smoke_static.step.dependOn(b.getInstallStep());
smoke_step.dependOn(&smoke_static.step);
const zipexe_name = if (target.result.os.tag == .windows) tclsh_static ++ "zip.exe" else tclsh_static ++ "zip";
const smoke_zip = b.addSystemCommand(&.{b.pathJoin(&.{ b.install_path, "bin", zipexe_name })});
common.scrubTclEnv(smoke_zip);
smoke_zip.has_side_effects = true;
smoke_zip.addFileArg(b.path("tools/suite_smoke.tcl"));
smoke_zip.addArgs(&.{ "-mode", "zip", "-expect", tcl_h_patchlevel });
smoke_zip.step.dependOn(&install_mkimg.step);
smoke_step.dependOn(&smoke_zip.step);
//-- tklib: pure-tcl but its installer is NOT a straight copy (module
//exclusions + aggregating pkgIndex generation). Batch mode, packages only;
//the -pkg-path is a cached output dir installed into the prefix.
const tklib_ver = common.installerPackageVersion(b, "../tklib");
const tklib_step = b.step("tklib", "install tklib via its own installer under the suite-built shell (+Tk smoke)");
const tklib_run = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(tklib_run);
tklib_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
//cached (non-side-effect) Runs spawn with stdout IGNORED - on windows the
//child then has no stdout handle and tcl errors on any puts ('can not find
//channel named "stdout"', critcl's logging). Capturing gives it a real pipe.
_ = tklib_run.captureStdOut(.{});
tklib_run.setCwd(b.path("../tklib"));
tklib_run.addFileArg(b.path("../tklib/installer.tcl"));
tklib_run.addArgs(&.{ "-no-gui", "-no-wait", "-no-apps", "-no-html", "-no-nroff", "-no-examples", "-pkg-path" });
const tklib_out = tklib_run.addOutputDirectoryArg("tklib-pkg");
const tklib_install = b.addInstallDirectory(.{
.source_dir = tklib_out,
.install_dir = .prefix,
.install_subdir = b.fmt("lib/tklib{s}", .{tklib_ver}),
});
const tklib_smoke = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(tklib_smoke);
tklib_smoke.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
tklib_smoke.has_side_effects = true;
tklib_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tklib_smoke.addArgs(&.{ "-gui", "1", "tooltip" });
tklib_smoke.step.dependOn(&tklib_install.step);
tklib_smoke.step.dependOn(b.getInstallStep()); //Tk dll + pkgIndex + tcl library
tklib_step.dependOn(&tklib_smoke.step);
//-- tcllib: same installer family (critcl DSL stubbed to no-ops - this is the
//PURE-TCL side; the critcl-built tcllibc accelerators are the next step)
const tcllib_ver = common.installerPackageVersion(b, "../tcllib");
const tcllib_step = b.step("tcllib", "install tcllib (pure-tcl side) via its own installer under the suite-built shell (+smoke)");
const tcllib_run = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(tcllib_run);
tcllib_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
//cached (non-side-effect) Runs spawn with stdout IGNORED - on windows the
//child then has no stdout handle and tcl errors on any puts ('can not find
//channel named "stdout"', critcl's logging). Capturing gives it a real pipe.
_ = tcllib_run.captureStdOut(.{});
tcllib_run.setCwd(b.path("../tcllib"));
tcllib_run.addFileArg(b.path("../tcllib/installer.tcl"));
tcllib_run.addArgs(&.{ "-no-gui", "-no-wait", "-no-apps", "-no-html", "-no-nroff", "-no-examples", "-pkg-path" });
const tcllib_out = tcllib_run.addOutputDirectoryArg("tcllib-pkg");
const tcllib_install = b.addInstallDirectory(.{
.source_dir = tcllib_out,
.install_dir = .prefix,
.install_subdir = b.fmt("lib/tcllib{s}", .{tcllib_ver}),
});
const tcllib_smoke = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(tcllib_smoke);
tcllib_smoke.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
tcllib_smoke.has_side_effects = true;
tcllib_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tcllib_smoke.addArgs(&.{ "md5", "sha1", "cmdline" });
tcllib_smoke.step.dependOn(&tcllib_install.step);
tcllib_smoke.step.dependOn(b.getInstallStep());
tcllib_step.dependOn(&tcllib_smoke.step);
//-- tcllibc: tcllib's critcl accelerators, critcl driving zig cc (the
//tracked critcl_zig.config; zig resolved from PATH with our own dir
//prepended). sak.tcl's critcl target expects a tclkitsh on windows - this
//replicates its underlying invocation directly, as suite.tcl did.
const tcllibc_step = b.step("tcllibc", "build+install tcllibc (critcl accelerators, zig cc) under the suite-built shell (+accel smoke)");
const critcl_run = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(critcl_run);
critcl_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
//cached (non-side-effect) Runs spawn with stdout IGNORED - on windows the
//child then has no stdout handle and tcl errors on any puts ('can not find
//channel named "stdout"', critcl's logging). Capturing gives it a real pipe.
_ = critcl_run.captureStdOut(.{});
critcl_run.setCwd(b.path("../tcllib"));
critcl_run.addFileArg(b.path("../critcl/main.tcl"));
critcl_run.addArg("-config");
critcl_run.addFileArg(b.path("critcl_zig.config"));
critcl_run.addArg("-cache");
_ = critcl_run.addOutputDirectoryArg("critcl-cache");
critcl_run.addArg("-force");
critcl_run.addArg("-libdir");
const critcl_libout = critcl_run.addOutputDirectoryArg("critcl-lib");
critcl_run.addArgs(&.{ "-pkg", "tcllibc" });
for (common.tcllibCritclFiles(b, "../tcllib")) |cf| {
critcl_run.addFileArg(b.path(b.pathJoin(&.{ "../tcllib/modules", cf })));
}
const zigdir = std.fs.path.dirname(b.graph.zig_exe) orelse ".";
const inherited_path = b.graph.environ_map.get("PATH") orelse "";
critcl_run.setEnvironmentVariable("PATH", b.fmt("{s}{c}{s}", .{ zigdir, std.fs.path.delimiter, inherited_path }));
const tcllibc_install = b.addInstallDirectory(.{
.source_dir = critcl_libout,
.install_dir = .prefix,
.install_subdir = "lib",
});
const tcllibc_smoke = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(tcllibc_smoke);
tcllibc_smoke.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
tcllibc_smoke.has_side_effects = true;
tcllibc_smoke.addFileArg(b.path("tools/pkg_smoke.tcl"));
tcllibc_smoke.addArgs(&.{ "-accel", "md5", "tcllibc" });
tcllibc_smoke.step.dependOn(&tcllibc_install.step);
tcllibc_smoke.step.dependOn(&tcllib_install.step); //accel host packages come from tcllib
tcllibc_smoke.step.dependOn(b.getInstallStep());
tcllibc_step.dependOn(&tcllibc_smoke.step);
//-- test-gate: the core testsuite under the built shell, gated on parsed
//totals vs the tracked dispositioned baseline (all.tcl's exit code lies)
const gate_step = b.step("test-gate", "tcl core testsuite under the built 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 gate_run = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(gate_run);
gate_run.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
gate_run.has_side_effects = true;
gate_run.addFileArg(b.path("tools/test_gate.tcl"));
gate_run.addArg("-testsdir");
gate_run.addArg(common.replaceAll(b, b.pathFromRoot(tcl_source_folder ++ "/tests"), "\\", "/"));
gate_run.addArg("-baseline");
gate_run.addFileArg(b.path("expected_test_failures.txt"));
gate_run.addArg("-logfile");
gate_run.addArg(common.replaceAll(b, b.pathFromRoot("../tcltest.log"), "\\", "/"));
if (testargs_opt.len != 0) {
gate_run.addArgs(&.{ "-testargs", testargs_opt });
}
gate_run.step.dependOn(b.getInstallStep());
gate_step.dependOn(&gate_run.step);
}
// ==================
}
//G-102 bootstrap mode: registered when this recipe is invoked from the TRACKED suite
@ -2252,6 +2422,8 @@ fn bootstrapMode(b: *std.Build) !void {
"build_tclvfs",
"build_tclthread",
"build_tk",
"critcl_zig.config",
"expected_test_failures.txt",
"patches",
"src",
"tools",
@ -2268,7 +2440,7 @@ fn bootstrapMode(b: *std.Build) !void {
//suite.tcl performs after its fossil staging. Per-zig-version cache dir (object
//caches must never be shared across zig versions - user-confirmed hazard), same
//name suite.tcl derives so both flows share the staged cache.
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries make-zipfs)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "make-zipfs" });
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries make-zipfs smoke tklib tcllib tcllibc - the full pipeline short of test-gate)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "make-zipfs", "smoke", "tklib", "tcllib", "tcllibc" });
var cachever: []const u8 = builtin.zig_version_string;
cachever = common.replaceAll(b, cachever, "+", "_");
cachever = common.replaceAll(b, cachever, "/", "_");

54
src/buildsuites/suite_tcl90/build_common.zig

@ -42,6 +42,60 @@ pub fn replaceAll(b: *std.Build, input: []const u8, needle: []const u8, replacem
return std.mem.replaceOwned(u8, b.allocator, input, needle, replacement) catch @panic("OOM");
}
//hermetic child shells (G-102): 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). suite.tcl scrubbed these for its
//whole process; zig-driven runs scrub per step.
pub fn scrubTclEnv(run: *std.Build.Step.Run) void {
for ([_][]const u8{ "TCLLIBPATH", "TCL_LIBRARY", "TK_LIBRARY" }) |ev| {
run.removeEnvironmentVariable(ev);
}
}
//tcllib-family installer trees carry 'package_version <v>' in
//support/installation/version.tcl
pub fn installerPackageVersion(b: *std.Build, tree_from_root: []const u8) []const u8 {
const data = readSourceFile(b, b.fmt("{s}/support/installation/version.tcl", .{tree_from_root}));
var lines = std.mem.splitScalar(u8, data, '\n');
while (lines.next()) |line| {
var toks = std.mem.tokenizeAny(u8, line, " \t\r");
const first = toks.next() orelse continue;
if (!std.mem.eql(u8, first, "package_version")) continue;
const ver = toks.next() orelse continue;
return b.dupe(ver);
}
std.debug.panic("no package_version in {s}/support/installation/version.tcl", .{tree_from_root});
}
//tcllib's critcl module files from its version.tcl declarations: the critcl_main
//module's files first, then every 'critcl' module's files (sak.tcl semantics -
//the same derivation suite.tcl's tcllibc block used). Returned paths are relative
//to the tcllib tree's modules/ dir. Tcl brace-list tokens are unwrapped by brace
//stripping (the declared file names contain no spaces).
pub fn tcllibCritclFiles(b: *std.Build, tcllib_from_root: []const u8) []const []const u8 {
const data = readSourceFile(b, b.fmt("{s}/support/installation/version.tcl", .{tcllib_from_root}));
var mains = std.array_list.Managed([]const u8).init(b.allocator);
var mods = std.array_list.Managed([]const u8).init(b.allocator);
var lines = std.mem.splitScalar(u8, data, '\n');
while (lines.next()) |line| {
var toks = std.mem.tokenizeAny(u8, line, " \t\r");
const first = toks.next() orelse continue;
const is_main = std.mem.eql(u8, first, "critcl_main");
if (!is_main and !std.mem.eql(u8, first, "critcl")) continue;
_ = toks.next() orelse continue; //module name
while (toks.next()) |tok| {
var f = tok;
f = std.mem.trim(u8, f, "{}");
if (f.len == 0) continue;
(if (is_main) &mains else &mods).append(b.dupe(f)) catch @panic("OOM");
}
}
if (mains.items.len == 0) @panic("no critcl_main declaration found in tcllib version.tcl");
mains.appendSlice(mods.items) catch @panic("OOM");
return mains.items;
}
//TCL_WIN_VERSION / TK_WIN_VERSION as the makefiles derive them:
//<dotversion>.<releaselevel>.<patchlevel with a/b/. stripped>, releaselevel
//0=alpha 1=beta 2=final (e.g 9.0 + 9.0.5 -> 9.0.2.905)

12
src/buildsuites/suite_tcl90/build_tclthread/build_tclthread.zig

@ -3,7 +3,8 @@ const fs = std.fs;
const builtin = @import("builtin");
const common = @import("../build_common.zig");
const mkvar_thread_zip_file = "libthread3.0.1.zip";
const mkvar_thread_version = "3.0.1";
const mkvar_thread_zip_file = "libthread" ++ mkvar_thread_version ++ ".zip";
const mkvar_thread_vfs_root = "libthread.vfs";
const mkvar_thread_vfs_path = mkvar_thread_vfs_root ++ "/thread_library";
@ -165,5 +166,14 @@ pub fn build_tclthread(comptime tcldir: []const u8, comptime subdir: []const u8,
}); //ignored for non-windows targets
b.installArtifact(lib);
//G-102: pkgIndex.tcl so 'package require Thread' resolves against the prefix
//(punkshell runtests -jobs and codethread need it) - formerly a suite.tcl
//post-build block.
const wf_pkgidx = b.addWriteFiles();
const pkgidx = wf_pkgidx.add("pkgIndex.tcl", "package ifneeded Thread " ++ mkvar_thread_version ++
" [list load [file normalize [file join $dir .. .. bin " ++ mkvar_pkg_lib_file ++ "]] Thread]\n");
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, "lib/thread" ++ mkvar_thread_version ++ "/pkgIndex.tcl");
b.getInstallStep().dependOn(&pkgidx_install.step);
return lib;
}

39
src/buildsuites/suite_tcl90/build_tclvfs/build_tclvfs_shared.zig

@ -1,9 +1,48 @@
const std = @import("std");
const fs = std.fs;
const common = @import("../build_common.zig");
const tclvfs_dotversion = "1.4.2";
const tclvfs_nodotversion = "142";
//G-102: install the tclvfs script package (lib/vfs<ver>) into the prefix - the two
//configure-products (vfs.tcl, pkgIndex.tcl) generated from their .in templates at
//configure time, the library scripts + template dir copied, and the dll alongside
//(vfs.tcl loads it from its own dir via ::vfs::self). Formerly a suite.tcl
//post-build block. Windows dll naming - the cross-target story is G-105's.
pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs_compile: *std.Build.Step.Compile) !void {
const pkgsub = "lib/vfs" ++ tclvfs_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 = pkgsub ++ "/template",
});
b.getInstallStep().dependOn(&install_template.step);
const wf = b.addWriteFiles();
inline for (.{
.{ "library/vfs.tcl.in", "vfs.tcl" },
.{ "pkgIndex.tcl.in", "pkgIndex.tcl" },
}) |pair| {
var t: []const u8 = common.readSourceFile(b, subdir ++ "/" ++ pair[0]);
t = common.replaceAll(b, t, "@PACKAGE_VERSION@", tclvfs_dotversion);
t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", "tcl9vfs" ++ tclvfs_nodotversion ++ ".dll");
t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", "tclvfs" ++ tclvfs_nodotversion ++ ".dll");
const gen = wf.add(pair[1], t);
const inst = b.addInstallFileWithDir(gen, .prefix, pkgsub ++ "/" ++ pair[1]);
b.getInstallStep().dependOn(&inst.step);
}
const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, pkgsub ++ "/tcl9vfs" ++ tclvfs_nodotversion ++ ".dll");
b.getInstallStep().dependOn(&dll_inst.step);
}
pub fn vfsadd_tclvfs_files(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, vfswrite: *std.Build.Step.WriteFile) !*std.Build.Step.WriteFile {
//_ = tcldir;
//const version = "1.4.2";

15
src/buildsuites/suite_tcl90/build_tk/build_tk.zig

@ -180,8 +180,7 @@ pub fn build_tk(comptime tcldir: []const u8, comptime subdir: []const u8, b: *st
b.installArtifact(lib);
//tk script library -> <prefix>/lib/tk9.0 (pkgIndex.tcl is generated by the suite
//driver post-build, per the makefile.vc install-binaries convention)
//tk script library -> <prefix>/lib/tk9.0
const install_tklib = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library"),
.install_dir = .prefix,
@ -189,5 +188,17 @@ pub fn build_tk(comptime tcldir: []const u8, comptime subdir: []const u8, b: *st
});
b.getInstallStep().dependOn(&install_tklib.step);
//G-102: pkgIndex.tcl per the makefile.vc install-binaries convention - formerly
//a suite.tcl post-build block.
const wf_pkgidx = b.addWriteFiles();
const pkgidx = wf_pkgidx.add("pkgIndex.tcl", b.fmt(
\\if {{[catch {{package present Tcl 9.0-}}]}} return
\\package ifneeded tk {s} [list load [file normalize [file join $dir .. .. bin tcl9tk90.dll]]]
\\package ifneeded Tk {s} [list load [file normalize [file join $dir .. .. bin tcl9tk90.dll]]]
\\
, .{ tk_h_patchlevel, tk_h_patchlevel }));
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, "lib/tk9.0/pkgIndex.tcl");
b.getInstallStep().dependOn(&pkgidx_install.step);
return lib;
}

12
src/buildsuites/suite_tcl90/sources.config

@ -1,5 +1,13 @@
# sources.config - suite_tcl90 source-pull declarations. THIS is the tweak surface
# for retargeting the suite at different repositories/branches/tags.
# sources.config - suite_tcl90 source-pull declarations for the FOSSIL DEV FLOW
# (live checkouts driven by suite.tcl). This is the tweak surface for retargeting
# that flow at different repositories/branches/tags.
#
# G-102: the no-tclsh bootstrap flow ('zig build stage|bootstrap') has its own
# equivalent surface, build.zig.zon - content-hashed PER-CHECKIN pins rather than
# live refs. The dependency names there match the source names here, and both
# flows materialize into the same recipe-coupled stage dirs. When retargeting,
# tweak whichever flow you drive (or both, to keep them aligned): refs here may
# float (branches); zon pins are exact by design.
#
# Read by suite.tcl as plain line records - the file is parsed, never executed.
# Format (whitespace separated, one record per line, # comments and blanks ignored):

353
src/buildsuites/suite_tcl90/suite.tcl

@ -1,13 +1,19 @@
#!/usr/bin/env tclsh
#suite_tcl90 driver (G-096): stage the tracked zig recipe into ../_build/suite_tcl90,
#arrange sources, generate the configure-produced files fresh checkouts lack, build,
#and smoke-test the result.
#suite_tcl90 dev driver (G-096, thinned under G-102): stage the tracked zig recipe
#into ../_build/suite_tcl90, arrange sources from LIVE fossil/git checkouts, and
#delegate the build/test pipeline to the staged zig recipe's steps.
#
#This is the FOSSIL DEV FLOW - convenient live-branch work against checkouts. The
#pipeline itself (configure-product generation, compile, package installs, smokes,
#test gate) lives entirely in the zig recipe; the equivalent no-tclsh flow is
#'zig build stage|bootstrap' from this directory, fetching the PINNED sources
#declared in build.zig.zon instead of checkouts (see README).
#
#Usage: tclsh suite.tcl build ?options?
# tclsh suite.tcl test ?-testargs {tcltest args}? (e.g -testargs {-file "assocd.test util.test"})
# tclsh suite.tcl clean
#Options:
# -zig <path> zig executable (default: $env(PUNK_ZIG), else 'zig' on PATH)
# -zig <path> zig executable (default: $env(PUNK_ZIG), else pinned bin/tools zig, else 'zig' on PATH)
# -optimize <mode> Debug|ReleaseSafe|ReleaseFast|ReleaseSmall (default ReleaseFast)
# -tclbranch <branch> override the 'tcl' ref from sources.config for this run
# (default: the ref declared in sources.config)
@ -15,7 +21,8 @@
# -repofolder <dir> fossil clone store (default ~/.punkshell/fossils; env PUNK_FOSSIL_STORE)
# -seedfossils <dir> read-only seed collection for missing clones (default ~/.fossils; "" disables)
# -all <0|1> with 'clean': reserved (the clone store lives outside the stage)
# -steps <list> zig build steps (default {install install-libraries make-zipfs})
# -steps <list> zig build steps (default {install install-libraries make-zipfs
# smoke tklib tcllib tcllibc} - the full pipeline short of test-gate)
#
#The build area lives entirely under src/buildsuites/_build (VCS-ignored via the
#existing _build globs). Fossil clones live in the PUNKSHELL-OWNED machine-level
@ -47,6 +54,7 @@ set suiteroot [file dirname [file normalize [info script]]]
#stage derived from the suite FOLDER name so a copied tree (copy-and-tweak workflow,
#see sources.config) stages into its own _build/<name> - copies stay isolated.
set stage [file normalize [file join $suiteroot .. _build [file tail $suiteroot]]]
set builddir [file join $stage build]
#-- options ---------------------------------------------------------------
set action [lindex $argv 0]
@ -58,7 +66,7 @@ array set opt {
-optimize ReleaseFast
-tclbranch {}
-refresh 0
-steps {install install-libraries make-zipfs}
-steps {install install-libraries make-zipfs smoke tklib tcllib tcllibc}
-testargs {}
-repofolder {}
-all 0
@ -104,6 +112,35 @@ if {$opt(-repofolder) ne ""} {
set fossildir [file join [file normalize $homedir] .punkshell fossils]
}
#Hermetic child shells: the suite-built tclsh must not see the user's machine-level
#package paths (TCLLIBPATH pushed e.g C:/TCLPKGS into tcl_pkgPath here - misdirecting
#installer default paths and risking false-positive smokes via machine packages).
#The zig recipe additionally scrubs these per built-shell Run step (G-102).
foreach ev {TCLLIBPATH TCL_LIBRARY TK_LIBRARY} {
if {[info exists env($ev)]} {
log "unsetting inherited \$env($ev) for hermetic child shells"
unset env($ev)
}
}
proc run_zig_steps {steps {extraopts {}}} {
#drive the STAGED recipe (suite.tcl parity contract with 'zig build bootstrap':
#cwd = staged build dir, per-zig-version cache, forward-slash prefix)
global opt builddir stage
if {![file exists [file join $builddir build905.zig]]} {
fail "no staged recipe at $builddir - run 'suite.tcl build' first"
}
set zigv [exec $opt(-zig) version]
log "zig: $opt(-zig) ($zigv)"
#Per-zig-version local cache: object caches must never be shared across zig
#versions (observed producing stale/duplicate artifacts in the link).
set cachedir ".zig-cache-[string map {+ _ / _ : _} $zigv]"
set savedpwd [pwd]
cd $builddir
run $opt(-zig) build --build-file build905.zig --cache-dir $cachedir --prefix [file join $stage out] -Doptimize=$opt(-optimize) {*}$extraopts {*}$steps
cd $savedpwd
}
if {$action eq "clean"} {
log "removing $stage (the clone store at $fossildir is untouched)"
file delete -force $stage
@ -111,66 +148,19 @@ if {$action eq "clean"} {
}
if {$action eq "test"} {
#Exercise the Tcl core test suite on the suite-built static shell and GATE on the
#parsed totals (all.tcl's exit code does NOT reflect failures): every failed test
#must appear in the tracked expected-failure baseline
#(suite_tcl90/expected_test_failures.txt) or the action fails listing the
#unexplained names. Full output is written to <stage>/tcltest.log.
set exe [file join $stage out bin tclsh90s.exe]
set testsdir [file join $stage tcl905 tests]
if {![file exists $exe]} {fail "no built shell at $exe - run 'suite.tcl build' first"}
if {![file isdirectory $testsdir]} {fail "no tcl tests dir at $testsdir - run 'suite.tcl build' first"}
set logfile [file join $stage tcltest.log]
log "tcl testsuite on $exe [expr {[llength $opt(-testargs)] ? $opt(-testargs) : "(full suite)"}] -> $logfile"
set savedpwd [pwd]
cd $testsdir
catch {exec $exe all.tcl {*}$opt(-testargs) > $logfile 2>@1}
cd $savedpwd
set f [open $logfile r]; set testout [read $f]; close $f
if {![regexp {all\.tcl:\s+Total\s+(\d+)\s+Passed\s+(\d+)\s+Skipped\s+(\d+)\s+Failed\s+(\d+)} $testout -> ntotal npassed nskipped nfailed]} {
fail "no all.tcl totals line found in $logfile - run did not complete"
#Delegates to the recipe's test-gate step (tools/test_gate.tcl under the
#suite-built shell): core testsuite gated on parsed totals vs the tracked
#expected_test_failures.txt baseline. Full output: <stage>/tcltest.log.
set extraopts {}
if {[llength $opt(-testargs)]} {
lappend extraopts -Dtestargs=$opt(-testargs)
}
log "totals: $ntotal run, $npassed passed, $nskipped skipped, $nfailed failed"
set failednames {}
foreach {- tname} [regexp -all -inline -line {^==== (\S+) FAILED} $testout] {
if {$tname ni $failednames} {lappend failednames $tname}
}
#baseline: one test name per line; '#' comments carry the disposition reasons
set baseline {}
set basefile [file join $suiteroot expected_test_failures.txt]
if {[file exists $basefile]} {
set f [open $basefile 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}
}
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]} {
log "note: [llength $stale] baseline entries did not fail this run (candidates for removal): $stale"
}
if {[llength $unexplained]} {
puts stderr "suite_tcl90 TEST GATE FAIL: [llength $unexplained] failed test(s) not in the expected-failure baseline:"
foreach tname $unexplained {puts stderr " $tname"}
puts stderr "(full output: $logfile; baseline: $basefile)"
exit 1
}
log "TEST GATE PASS ([llength $failednames] failures, all baselined)"
run_zig_steps {test-gate} $extraopts
exit 0
}
#-- action: build ----------------------------------------------------------
#-- fossil environment ----------------------------------------------------
#Checkout registrations go to a stage-local config db, not the user's global one
#(agent-memory: fossil scratchpad registry pollution). Clones are still shared.
@ -178,16 +168,6 @@ file mkdir $stage
set env(FOSSIL_HOME) $stage
file mkdir $fossildir
#Hermetic child shells: the suite-built tclsh must not see the user's machine-level
#package paths (TCLLIBPATH pushed e.g C:/TCLPKGS into tcl_pkgPath here - misdirecting
#installer default paths and risking false-positive smokes via machine packages).
foreach ev {TCLLIBPATH TCL_LIBRARY TK_LIBRARY} {
if {[info exists env($ev)]} {
log "unsetting inherited \$env($ev) for hermetic child shells"
unset env($ev)
}
}
proc fossil_source {name url branch dir} {
#ensure <store>/<name>.fossil exists (read-only seed copy, else network clone) and
#open it at branch in dir
@ -226,22 +206,35 @@ proc fossil_source {name url branch dir} {
}
}
proc git_source {name url ref dir} {
#clone (or leave) a git checkout pinned at ref (critcl - the recipe's tcllibc
#step consumes it from the stage)
global opt
if {![file isdirectory [file join $dir .git]]} {
log "cloning $name (git) -> $dir"
run git clone --quiet $url $dir
} elseif {$opt(-refresh)} {
run git -C $dir fetch --quiet
}
run git -C $dir checkout --quiet $ref
}
#-- stage the recipe ------------------------------------------------------
#Per-item sync (not a wholesale delete of the build dir): zig cache dirs live in the
#build dir and are kept - they are per-zig-version (see --cache-dir below), and windows
#can hold transient locks on recently-used cache trees.
set builddir [file join $stage build]
#build dir and are kept - they are per-zig-version (see run_zig_steps), and windows
#can hold transient locks on recently-used cache trees. Item list mirrors the zig
#bootstrap mode's recipe_items (build905.zig bootstrapMode).
log "staging recipe -> $builddir"
file mkdir $builddir
foreach item {build905.zig build_common.zig build_libtommath build_zlib build_tclvfs build_tclthread build_tk patches src tools} {
foreach item {build905.zig build_common.zig build_libtommath build_zlib build_tclvfs build_tclthread build_tk critcl_zig.config expected_test_failures.txt patches src tools} {
file delete -force [file join $builddir $item]
file copy [file join $suiteroot $item] $builddir
}
#-- sources ---------------------------------------------------------------
#Declared in sources.config (the copy-and-tweak surface - see its header for the
#format, the copy workflow, and the recipe-coupling depth limit). Parsed as line
#records - never executed.
#Declared in sources.config (the copy-and-tweak surface for the LIVE-checkout dev
#flow - see its header; build.zig.zon is the equivalent PINNED surface for the
#no-tclsh flow). Parsed as line records - never executed.
proc load_sources_config {path} {
set sources [dict create]
set f [open $path r]; set data [read $f]; close $f
@ -268,13 +261,15 @@ foreach required {tcl tclthread tclvfs tk tklib tcllib critcl} {
}
}
foreach name {tcl tclthread tclvfs tk tklib tcllib} {
set s [dict get $sources $name]
dict for {name s} $sources {
set ref [dict get $s ref]
if {$name eq "tcl" && $opt(-tclbranch) ne ""} {
set ref $opt(-tclbranch) ;#per-run CLI override of the declared tcl ref
}
fossil_source $name [dict get $s url] $ref [file join $stage [dict get $s dir]]
switch -- [dict get $s kind] {
fossil {fossil_source $name [dict get $s url] $ref [file join $stage [dict get $s dir]]}
git {git_source $name [dict get $s url] $ref [file join $stage [dict get $s dir]]}
}
}
#zlib: the recipe compiles the tcl tree's own vendored copy in place
@ -282,190 +277,16 @@ foreach name {tcl tclthread tclvfs tk tklib tcllib} {
#vqtcl/vlerq: removed from the suite (user 2026-07-20) - old-tclkit experiment with no
#confirmed public upstream. Future tclkit work: TEMP_REFERENCE/metakit + KitCreator.
#-- version facts for smokes/logging ---------------------------------------
#G-102: the configure-products a fresh checkout lacks (tclUuid.h, tclsh.exe.manifest,
#the thread.rc tcl.h include, tkUuid.h, wish.exe.manifest) are now generated INSIDE
#the zig recipe as build-cache overlays - source checkouts are never written. What
#remains here is read-only: version facts for the smoke expectations and the log.
set tcldir [file join $stage tcl905]
set f [open [file join $tcldir manifest.uuid] r]; set uuid [read $f]; close $f
set f [open [file join $tcldir generic tcl.h] r]; set tclh [read $f]; close $f
#-- version facts for the final log ----------------------------------------
#G-102: configure-product generation, package installs, smokes and the test gate all
#live in the zig recipe now (build-cache overlays + steps under the suite-built
#shell) - source checkouts are never written. Read-only facts for the log only.
set f [open [file join $stage tcl905 manifest.uuid] r]; set uuid [read $f]; close $f
set f [open [file join $stage tcl905 generic tcl.h] r]; set tclh [read $f]; close $f
if {![regexp {#\s*define\s+TCL_PATCH_LEVEL\s+"([^"]+)"} $tclh -> patchlevel]} {fail "TCL_PATCH_LEVEL not found in tcl.h"}
set tkdir [file join $stage tk9]
set f [open [file join $tkdir generic tk.h] r]; set tkh [read $f]; close $f
if {![regexp {#\s*define\s+TK_VERSION\s+"([^"]+)"} $tkh -> tkversion]} {fail "TK_VERSION not found in tk.h"}
if {![regexp {#\s*define\s+TK_PATCH_LEVEL\s+"([^"]+)"} $tkh -> tkpatchlevel]} {fail "TK_PATCH_LEVEL not found in tk.h"}
log "sources: tcl $patchlevel (checkout [string range $uuid 0 11]...), tk $tkpatchlevel"
#-- build -----------------------------------------------------------------
set zigv [exec $opt(-zig) version]
log "zig: $opt(-zig) ($zigv)"
#Per-zig-version local cache: object caches must never be shared across zig versions
#(observed producing stale/duplicate artifacts in the link; user-confirmed hazard).
set cachedir ".zig-cache-[string map {+ _ / _ : _} $zigv]"
set savedpwd [pwd]
cd $builddir
run $opt(-zig) build --build-file build905.zig --cache-dir $cachedir --prefix [file join $stage out] -Doptimize=$opt(-optimize) {*}$opt(-steps)
cd $savedpwd
#-- thread pkgIndex (G-098: the recipe installs tcl9thread<NNN>.dll but no index;
#punkshell runtests/-jobs and codethread need 'package require Thread' to resolve) --
set threaddlls [glob -nocomplain [file join $stage out bin tcl9thread*.dll]]
if {[llength $threaddlls]} {
set tdll [file tail [lindex $threaddlls 0]]
if {[regexp {tcl9thread(\d)(\d)(\d)\.dll} $tdll -> tj tn tp]} {
set threadver "$tj.$tn.$tp"
set tpkgdir [file join $stage out lib thread$threadver]
file mkdir $tpkgdir
set f [open [file join $tpkgdir pkgIndex.tcl] w]
fconfigure $f -translation lf
puts $f "package ifneeded Thread $threadver \[list load \[file normalize \[file join \$dir .. .. bin $tdll\]\] Thread\]"
close $f
log "thread pkgIndex.tcl written ($tpkgdir -> $tdll)"
}
}
#-- vfs package install (G-098): the recipe builds tcl9vfs142.dll only; punkshell's
#repl code interp wants vfs/vfs::zip (modpod mounting). Install the tclvfs script
#package with its two configure-products generated (vfs.tcl, pkgIndex.tcl) and the
#dll alongside (vfs.tcl loads from its own dir via ::vfs::self).
set vfssrc [file join $stage tclvfs]
if {[file isdirectory $vfssrc]} {
set f [open [file join $vfssrc configure.ac] r]; set cac [read $f]; close $f
if {![regexp {AC_INIT\(\[vfs\],\[([^\]]+)\]} $cac -> vfsver]} {fail "vfs version not found in tclvfs configure.ac"}
set vpkgdir [file join $stage out lib vfs$vfsver]
if {![file isdirectory $vpkgdir]} {
log "installing tclvfs $vfsver package -> $vpkgdir"
file mkdir $vpkgdir
foreach tf [glob -directory [file join $vfssrc library] *.tcl] {
file copy -force $tf $vpkgdir
}
file copy -force [file join $vfssrc library template] $vpkgdir
set map [list @PACKAGE_VERSION@ $vfsver @PKG_LIB_FILE9@ tcl9vfs142.dll @PKG_LIB_FILE8@ tclvfs142.dll]
foreach {intmpl outname} [list [file join $vfssrc library vfs.tcl.in] vfs.tcl [file join $vfssrc pkgIndex.tcl.in] pkgIndex.tcl] {
set f [open $intmpl r]; set t [read $f]; close $f
set f [open [file join $vpkgdir $outname] w]
fconfigure $f -translation lf
puts -nonewline $f [string map $map $t]
close $f
}
file copy -force [file join $stage out bin tcl9vfs142.dll] $vpkgdir
}
}
#-- tk pkgIndex (makefile.vc install-binaries convention) ------------------
set tkpkgdir [file join $stage out lib tk$tkversion]
if {[file isdirectory $tkpkgdir]} {
set f [open [file join $tkpkgdir pkgIndex.tcl] w]
fconfigure $f -translation lf
puts $f "if {\[catch {package present Tcl 9.0-}\]} return"
puts $f "package ifneeded tk $tkpatchlevel \[list load \[file normalize \[file join \$dir .. .. bin tcl9tk90.dll\]\]\]"
puts $f "package ifneeded Tk $tkpatchlevel \[list load \[file normalize \[file join \$dir .. .. bin tcl9tk90.dll\]\]\]"
close $f
log "tk pkgIndex.tcl written ($tkpkgdir)"
}
log "sources: tcl $patchlevel (checkout [string range $uuid 0 11]...)"
#-- smoke -----------------------------------------------------------------
set exe [file join $stage out bin tclsh90s.exe]
set zexe [file join $stage out bin tclsh90szip.exe]
foreach {label path script} [list \
static $exe {puts [info patchlevel]} \
zip $zexe {puts "[info patchlevel] tz=[clock format 0 -gmt 0 -format %Z] auto=[llength [info procs auto_load]]"} \
] {
if {![file exists $path]} {fail "$label shell missing: $path"}
set out [exec $path << "$script; exit"]
log "$label shell: $out"
if {![string match "$patchlevel*" $out]} {fail "$label shell patchlevel mismatch: got '$out' want '$patchlevel'"}
}
#-- tklib install (G-098) --------------------------------------------------
#tklib is pure-tcl but its installer is NOT a straight copy: module exclusions and
#generation of the aggregating pkgIndex.tcl (gen_main_index). Run it in batch mode
#(packages only) with the suite-built shell; default paths derive from that shell,
#landing the packages under <out>/lib/<tklib-version-dir>.
proc installer_version {dir} {
#tcllib-family: support/installation/version.tcl carries 'package_version <v>'
set f [open [file join $dir support installation version.tcl] r]
set data [read $f]; close $f
if {![regexp {package_version\s+(\S+)} $data -> v]} {error "no package_version in $dir"}
return $v
}
if {![llength [glob -nocomplain [file join $stage out lib tklib*]]]} {
set tklibver [installer_version [file join $stage tklib]]
log "installing tklib $tklibver (batch, packages only) via its own installer"
set savedpwd [pwd]
cd [file join $stage tklib]
run $exe installer.tcl -no-gui -no-wait -no-apps -no-html -no-nroff -no-examples \
-pkg-path [file join $stage out lib tklib$tklibver]
cd $savedpwd
}
set tkliblanded [glob -nocomplain [file join $stage out lib tklib*]]
if {![llength $tkliblanded]} {fail "tklib install did not land under [file join $stage out lib]"}
set tksmoke [exec $exe << {package require Tk; wm withdraw .; puts "tooltip: [package require tooltip]"; destroy .; exit}]
log "tklib: [file tail [lindex $tkliblanded 0]] installed; smoke $tksmoke"
#-- tcllib install (G-098) -------------------------------------------------
#Same installer family as tklib (module exclusions + aggregating pkgIndex; critcl DSL
#stubbed to no-ops, so this is the PURE-TCL side only - the critcl-built tcllibc
#accelerators are a separate step). Batch, packages only, suite-built shell.
if {![llength [glob -nocomplain [file join $stage out lib tcllib*]]]} {
set tcllibver [installer_version [file join $stage tcllib]]
log "installing tcllib $tcllibver (batch, packages only, pure-tcl side) via its own installer"
set savedpwd [pwd]
cd [file join $stage tcllib]
run $exe installer.tcl -no-gui -no-wait -no-apps -no-html -no-nroff -no-examples \
-pkg-path [file join $stage out lib tcllib$tcllibver]
cd $savedpwd
}
set tclliblanded [glob -nocomplain [file join $stage out lib tcllib*]]
if {![llength $tclliblanded]} {fail "tcllib install did not land under [file join $stage out lib]"}
set tcllibsmoke [exec $exe << {puts "md5:[package require md5] sha1:[package require sha1] cmdline:[package require cmdline]"; exit}]
log "tcllib: [file tail [lindex $tclliblanded 0]] installed; smoke $tcllibsmoke"
#-- tcllibc accelerators via critcl + zig cc (G-098) -----------------------
#sak.tcl's critcl target expects a tclkitsh+starkit on windows; we replicate its
#underlying invocation directly (critcl -cache ... -force -libdir <dest> -pkg tcllibc
#<module critcl files>) using the critcl source checkout's main.tcl app under the
#suite shell, with the suite's zig on PATH and the tracked critcl_zig.config.
set critcldir [file join $stage [dict get $sources critcl dir]]
set critclpin [dict get $sources critcl ref]
if {![file isdirectory [file join $critcldir .git]]} {
log "cloning critcl (git) -> $critcldir"
run git clone --quiet [dict get $sources critcl url] $critcldir
}
run git -C $critcldir checkout --quiet $critclpin
if {![llength [glob -nocomplain [file join $stage out lib tcllibc*]]]} {
#module list from tcllib's own declarations (support/installation/version.tcl):
#critcl_main's file first, then every critcl module's files (sak.tcl semantics)
set f [open [file join $stage tcllib support installation version.tcl] r]
set vdata [read $f]; close $f
set mainfiles {}
set modfiles {}
foreach line [split $vdata \n] {
if {[regexp {^critcl_main\s+\S+\s+(.+)$} $line -> flist]} {
foreach ff [concat {*}$flist] {lappend mainfiles $ff}
} elseif {[regexp {^critcl\s+\S+\s+(.+)$} $line -> flist]} {
foreach ff [concat {*}$flist] {lappend modfiles $ff}
}
}
if {![llength $mainfiles]} {fail "no critcl_main declaration found in tcllib version.tcl"}
set critclfiles {}
foreach ff [concat $mainfiles $modfiles] {
lappend critclfiles [file join $stage tcllib modules $ff]
}
log "building tcllibc via critcl $critclpin + zig cc ([llength $critclfiles] critcl files)"
set zigdir [file dirname $opt(-zig)]
set savedpath $env(PATH)
set env(PATH) "[file nativename $zigdir];$env(PATH)"
set savedpwd [pwd]
cd [file join $stage tcllib]
run $exe [file join $critcldir main.tcl] \
-config [file join $suiteroot critcl_zig.config] \
-cache [file join $stage .critcl] -force \
-libdir [file join $stage out lib] -pkg tcllibc {*}$critclfiles
cd $savedpwd
set env(PATH) $savedpath
}
set tcllibcsmoke [exec $exe << {puts "tcllibc:[package require tcllibc]"; package require md5; puts "md5-critcl-accel:$::md5::accel(critcl)"; exit}]
log "tcllibc: $tcllibcsmoke"
#-- build (staged recipe steps) -------------------------------------------
run_zig_steps $opt(-steps)
log "PASS - built $patchlevel (checkout [string range $uuid 0 11]..., zig $zigv) -> $stage/out"
log "PASS - built $patchlevel (checkout [string range $uuid 0 11]...) -> $stage/out"

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

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

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

@ -0,0 +1,26 @@
#suite_smoke.tcl (G-102): smoke assertions run under a suite-built shell itself
#(the shell being smoked executes this script). Formerly suite.tcl inline smokes.
#
#args: -mode static|zip -expect <patchlevel>
# static: interpreter reports the expected patchlevel
# zip: additionally proves the ATTACHED library works - tzdata reachable
# (clock format with a named zone) and auto_load present
proc fail {msg} {puts stderr "suite_smoke FAIL: $msg"; flush stderr; exit 1}
array set opt {-mode static -expect {}}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k'"}
set opt($k) $v
}
if {$opt(-expect) eq ""} {fail "missing -expect <patchlevel>"}
set pl [info patchlevel]
if {$pl ne $opt(-expect)} {fail "patchlevel mismatch: got '$pl' want '$opt(-expect)' ([info nameofexecutable])"}
if {$opt(-mode) eq "zip"} {
if {[catch {clock format 0 -gmt 0 -format %Z} tz]} {fail "tzdata not reachable from attached library: $tz"}
if {[llength [info procs auto_load]] != 1} {fail "auto_load missing - attached library init incomplete"}
puts "suite_smoke OK: $pl (zip: tz=$tz auto=1) [info nameofexecutable]"
} else {
puts "suite_smoke OK: $pl [info nameofexecutable]"
}
exit 0

71
src/buildsuites/suite_tcl90/tools/test_gate.tcl

@ -0,0 +1,71 @@
#test_gate.tcl (G-102): run the Tcl core testsuite under THIS interpreter's own
#shell ([info nameofexecutable] - the suite-built tclsh when driven as the
#'zig build test-gate' step) and GATE on the parsed totals: all.tcl's exit code
#does NOT reflect failures, so every failed test must appear in the tracked
#dispositioned baseline or the gate fails listing the unexplained names.
#Formerly suite.tcl's 'test' action; suite.tcl now delegates here.
#
#args: -testsdir <dir> -baseline <file> -logfile <file> ?-testargs {tcltest args}?
proc fail {msg} {puts stderr "test_gate ERROR: $msg"; flush stderr; exit 1}
array set opt {-testsdir {} -baseline {} -logfile {} -testargs {}}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k' (expected -testsdir -baseline -logfile -testargs)"}
set opt($k) $v
}
foreach req {-testsdir -baseline -logfile} {
if {$opt($req) eq ""} {fail "missing required option $req"}
}
if {![file isdirectory $opt(-testsdir)]} {fail "no tests dir at $opt(-testsdir) - build first"}
if {![file exists $opt(-baseline)]} {fail "no baseline file at $opt(-baseline)"}
set exe [info nameofexecutable]
set logfile [file normalize $opt(-logfile)]
puts "test_gate: tcl testsuite under $exe [expr {[llength $opt(-testargs)] ? $opt(-testargs) : "(full suite)"}] -> $logfile"
flush stdout
set savedpwd [pwd]
cd $opt(-testsdir)
catch {exec $exe all.tcl {*}$opt(-testargs) > $logfile 2>@1}
cd $savedpwd
set f [open $logfile r]; set testout [read $f]; close $f
if {![regexp {all\.tcl:\s+Total\s+(\d+)\s+Passed\s+(\d+)\s+Skipped\s+(\d+)\s+Failed\s+(\d+)} $testout -> ntotal npassed nskipped nfailed]} {
fail "no all.tcl totals line found in $logfile - run did not complete"
}
puts "test_gate: totals: $ntotal run, $npassed passed, $nskipped skipped, $nfailed failed"
set failednames {}
foreach {- tname} [regexp -all -inline -line {^==== (\S+) FAILED} $testout] {
if {$tname ni $failednames} {lappend failednames $tname}
}
#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}
}
if {![llength $opt(-testargs)]} {
#only meaningful for full runs: baseline entries that no longer fail
set stale {}
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"
}
}
if {[llength $unexplained]} {
puts stderr "test_gate GATE FAIL: [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))"
exit 1
}
puts "test_gate: GATE PASS ([llength $failednames] failures, all baselined)"
exit 0
Loading…
Cancel
Save