Compare commits

...

8 Commits

Author SHA1 Message Date
Julian Noble 570e02b6a9 G-118 proposed: tclsh subcommand review - stock-parity gaps, piperepl residue, docs, console tests 1 week ago
Julian Noble f3a9067cbf runtimebash_wsl.test: tolerate G-103 metadata-era list row format (stale glob) 1 week ago
Julian Noble e61411c19a tclsh subcommand review: tclshcmd.test suite + contract doc corrections 1 week ago
Julian Noble 1dd9940695 tclsh subcommand: stock arg forms (leading dash, -encoding) + lib: refusal (0.19.0) 1 week ago
Julian Noble ee3ed05923 punk-runtime: dependency-free sha1 hashing in the powershell payload (Get-FileHash field failure) 1 week ago
Julian Noble 78d21c5de1 templates modpod: build-synced punk-runtime.cmd (toml-aware list -remote era) 1 week ago
Julian Noble a7ca13bf53 suite_tcl90: tclvfs -> forTcl9 branch (1.5.0); version-derived vfs names; familyrev default 2 1 week ago
Julian Noble cf8449e4c9 punk::mod 0.1.2: version-aware punk::apps latest selection (audit fix) 1 week ago
  1. 16
      CHANGELOG.md
  2. 4
      GOALS.md
  3. 8
      bin/AGENTS.md
  4. 47
      bin/punk-runtime.cmd
  5. 36
      goals/G-118-tclsh-subcommand-review.md
  6. 2
      punkproject.toml
  7. 6
      src/buildsuites/suite_tcl90/build.zig.zon
  8. 15
      src/buildsuites/suite_tcl90/build905.zig
  9. 134
      src/buildsuites/suite_tcl90/build_tclvfs/build_tclvfs_shared.zig
  10. 31
      src/buildsuites/suite_tcl90/expected_test_failures_tclvfs.txt
  11. 13
      src/buildsuites/suite_tcl90/patches/README.md
  12. 6
      src/buildsuites/suite_tcl90/sources.config
  13. 38
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd
  14. 14
      src/modules/punk/mod-999999.0a1.0.tm
  15. 3
      src/modules/punk/mod-buildversion.txt
  16. 47
      src/scriptapps/bin/punk-runtime.ps1
  17. 3
      src/tests/shell/AGENTS.md
  18. 3
      src/tests/shell/testsuites/binscripts/runtimebash_wsl.test
  19. 386
      src/tests/shell/testsuites/punkexe/tclshcmd.test
  20. 63
      src/vfs/_config/punk_main.tcl

16
CHANGELOG.md

@ -5,6 +5,22 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy. "Project Versioning" section for the bump policy.
## [0.19.0] - 2026-07-23
- `bin/punk-runtime.cmd`: sha1 hashing (fetch verification, `list -remote` comparison) no longer depends on `Get-FileHash` - a script-defined function in Windows PowerShell 5.1 that vanishes on machines with a damaged PSModulePath while compiled cmdlets keep working (observed in the field: "Get-FileHash ... not recognized" under PS 5, making `list -remote` report spurious UPDATE AVAILABLE and fetch verification fail closed). Hashing now uses .NET directly (`Get-PunkFileSha1`), working on any PowerShell edition/state, with a one-time informational note when the damaged condition is detected. The bash payload's multi-tool sha1 probing was already robust and is unchanged.
- `tclsh` subcommand: stock tclsh argument forms - a leading `-` argument no longer errors as a
script name: all arguments stay in `::argv` and the subcommand proceeds to the repl/stdin
evaluation, matching stock `tclsh - args...`; `-encoding name fileName` is recognised and the
script is sourced with the named encoding (incomplete `-encoding` forms fall through to argv,
as stock). `lib:` scriptlib names are refused with a pointer to the `script` subcommand
(exit 1) - the tclsh subcommand keeps plain tclsh semantics and loads no punk modules.
Pinned by `src/tests/shell/testsuites/punkexe/tclshcmd.test` (new suite, also covering the
piperepl launch-state contract on patched kits).
## [0.18.9] - 2026-07-22
- punk::mod 0.1.2: punk::apps `latest` app-version selection fixed (version-selection audit) - a plain lexical sort picked the LOWEST version as latest (and misorders 1.10 vs 1.9); now `package vcompare`-sorted highest, empty unversioned-main entries excluded. Audit outcome: all other punk-owned `[package versions]` consumers already sort with vcompare; naive sorts remain only in vendored third-party code (tcllib doctools, pattern-1.2.8.tm) - flagged, not edited, per the vendor no-edit policy.
## [0.18.8] - 2026-07-22 ## [0.18.8] - 2026-07-22
- `bin/punk-runtime.cmd` `list -remote`: remote-only rows skip support files (artifact metadata tomls etc now ride in the server's sha1sums beside runtimes - same extension set the local candidate filter excludes); the `(= server default)` active marker also recognizes a materialized working copy via its beside-toml artifact identity (the default names an immutable `-r<N>` artifact while the active is typically its working name - exact-name matching alone would never fire post-family). Verified live against the first published family artifacts. - `bin/punk-runtime.cmd` `list -remote`: remote-only rows skip support files (artifact metadata tomls etc now ride in the server's sha1sums beside runtimes - same extension set the local candidate filter excludes); the `(= server default)` active marker also recognizes a materialized working copy via its beside-toml artifact identity (the default names an immutable `-r<N>` artifact while the active is typically its working name - exact-name matching alone would never fire post-family). Verified live against the first published family artifacts.

4
GOALS.md

@ -393,3 +393,7 @@ Detail: goals/G-116-suite-built-tcltls.md
### G-117 [proposed] Self-describing family runtimes: embedded artifact record + metadata schema v1 ### G-117 [proposed] Self-describing family runtimes: embedded artifact record + metadata schema v1
Scope: src/buildsuites/suite_tcl90/ (kit-family staging embeds the record; family_artifacts.tcl schema v1 fields + emission ordering; -Doriginurl/-Dpackager options), tools/family_check.tcl (embedded-record verification), src/scriptapps/bin/punk-runtime.* + bin/punk-runtime.cmd via rewrap ('info' action; schema-tolerant parsing), punkbin AGENTS.md (record relationship + schema/field documentation) Scope: src/buildsuites/suite_tcl90/ (kit-family staging embeds the record; family_artifacts.tcl schema v1 fields + emission ordering; -Doriginurl/-Dpackager options), tools/family_check.tcl (embedded-record verification), src/scriptapps/bin/punk-runtime.* + bin/punk-runtime.cmd via rewrap ('info' action; schema-tolerant parsing), punkbin AGENTS.md (record relationship + schema/field documentation)
Detail: goals/G-117-self-describing-runtimes.md Detail: goals/G-117-self-describing-runtimes.md
### G-118 [proposed] tclsh subcommand review: stock-parity gaps, piperepl residue, docs and console-path tests
Scope: src/vfs/_config/punk_main.tcl (tclsh dispatch), src/vfs/_config/project_main.tcl (parity), src/buildsuites/suite_tcl90/patches/ (tclMain piperepl rev items), src/tests/shell/testsuites/punkexe/tclshcmd.test (coverage additions)
Detail: goals/G-118-tclsh-subcommand-review.md

8
bin/AGENTS.md

@ -101,6 +101,14 @@ The first argument to a built punk shell may be a dash-delimited package mode co
`bin/runtime/<platform>/` (retrieved from the punkbin artifact repo with sha1 `bin/runtime/<platform>/` (retrieved from the punkbin artifact repo with sha1
verification against its `sha1sums.txt` - both the powershell payload on windows and the verification against its `sha1sums.txt` - both the powershell payload on windows and the
bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`). bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`).
Hashing is deliberately dependency-free in both payloads: the powershell payload computes
sha1 via .NET (`Get-PunkFileSha1`) rather than `Get-FileHash`, because Get-FileHash is a
script-defined function in Windows PowerShell 5.1 resolved through PSModulePath and
vanishes on machines with a damaged PSModulePath while compiled cmdlets still work
(field-observed; a one-time note flags the condition); the bash payload probes
sha1sum/shasum/sha1/openssl. Keep new payload code free of PS 5.1
script-module-defined cmdlets (`Get-FileHash`, `New-TemporaryFile`, `New-Guid`,
`Import-PowerShellDataFile`, ...) for the same reason.
`list -remote` compares local runtimes against the server's sha1sums (Same version / `list -remote` compares local runtimes against the server's sha1sums (Same version /
UPDATE AVAILABLE / not-listed, plus remote-only entries; cached sha1sums.txt fallback UPDATE AVAILABLE / not-listed, plus remote-only entries; cached sha1sums.txt fallback
with a warning when the server is unreachable). It also surfaces the selection state: with a warning when the server is unreachable). It also surfaces the selection state:

47
bin/punk-runtime.cmd

@ -2513,6 +2513,41 @@ function Get-PunkRuntimeRootName {
} }
return $name return $name
} }
#Dependency-free SHA1 (robustness fix 2026-07-23): Get-FileHash is a SCRIPT-defined
#function in Windows PowerShell 5.1's Microsoft.PowerShell.Utility, resolved via
#PSModulePath autoloading - on machines with a damaged PSModulePath (pwsh7 module-path
#pollution, overridden env vars, redirected user module dirs) it vanishes while the
#module's COMPILED cmdlets (Write-Host, Invoke-WebRequest) still work, observed in the
#field ("Get-FileHash ... not recognized" under PS 5). Hashing here goes through .NET
#directly so fetch verification and list -remote comparison work on any PowerShell
#edition/state, with a one-time informational note when the damage is present.
#Returns lowercase hex (sha1sums.txt convention; -eq/-ne comparisons are
#case-insensitive so case never matters for verification).
$script:punkFileHashNoteShown = $false
function Get-PunkFileSha1 {
param([string] $Path)
if (-not $script:punkFileHashNoteShown) {
$script:punkFileHashNoteShown = $true
if ($PSVersionTable.PSVersion.Major -le 5 -and -not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {
Write-Host "note: Get-FileHash is unavailable in this Windows PowerShell (PSModulePath on this machine may be damaged) - punk-runtime uses built-in .NET hashing and is unaffected"
}
}
#Resolve-Path: .NET's current directory is not PowerShell's location, so hand
#[System.IO.File] an absolute provider path
$fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath
$sha1 = [System.Security.Cryptography.SHA1]::Create()
try {
$fs = [System.IO.File]::OpenRead($fullpath)
try {
$hashbytes = $sha1.ComputeHash($fs)
} finally {
$fs.Dispose()
}
} finally {
$sha1.Dispose()
}
return (($hashbytes | ForEach-Object { $_.ToString("x2") }) -join "")
}
#G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted #G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted
#by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a #by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a
#short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent.
@ -2787,8 +2822,8 @@ function psmain {
if (Test-Path -Path $output -PathType Leaf) { if (Test-Path -Path $output -PathType Leaf) {
Write-Host "Runtime already found at $output" Write-Host "Runtime already found at $output"
Write-Host "Checking sha1 checksum of local file versus sha1 of server file" Write-Host "Checking sha1 checksum of local file versus sha1 of server file"
$file_sha1 = Get-FileHash -Path "$output" -Algorithm SHA1 $file_sha1 = Get-PunkFileSha1 -Path "$output"
if (${file_sha1}.Hash -ne $stored_sha1) { if ($file_sha1 -ne $stored_sha1) {
Write-Host "$runtime on server has different sha1 hash - Download required" Write-Host "$runtime on server has different sha1 hash - Download required"
$need_download = $true $need_download = $true
} }
@ -2812,8 +2847,8 @@ function psmain {
} }
Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt" Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt"
Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?) Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?)
$newfile_sha1 = Get-FileHash -Path "${output}.tmp" -Algorithm SHA1 $newfile_sha1 = Get-PunkFileSha1 -Path "${output}.tmp"
if (${newfile_sha1}.Hash -eq $stored_sha1) { if ($newfile_sha1 -eq $stored_sha1) {
Write-Host "sha1 checksum ok" Write-Host "sha1 checksum ok"
Move-Item -Path "${output}.tmp" -Destination "${output}" -Force Move-Item -Path "${output}.tmp" -Destination "${output}" -Force
Write-Host "Runtime is available at ${output}" Write-Host "Runtime is available at ${output}"
@ -3045,8 +3080,8 @@ function psmain {
#local runtimes in the comparison #local runtimes in the comparison
$dircontents = Get-PunkRuntimeCandidates $archfolder $dircontents = Get-PunkRuntimeCandidates $archfolder
foreach ($f in $dircontents) { foreach ($f in $dircontents) {
$local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1 $local_sha1 = Get-PunkFileSha1 -Path $(${f}.FullName)
$localdict[$f.Name] = ${local_sha1}.Hash $localdict[$f.Name] = $local_sha1
} }
} }

36
goals/G-118-tclsh-subcommand-review.md

@ -0,0 +1,36 @@
# G-118 tclsh subcommand review
Status: proposed
Scope: src/vfs/_config/punk_main.tcl (tclsh dispatch), src/vfs/_config/project_main.tcl (parity), src/buildsuites/suite_tcl90/patches/ (tclMain piperepl rev items), src/tests/shell/testsuites/punkexe/tclshcmd.test (coverage additions)
Goal: The 'tclsh' subcommand has a settled, documented contract - each remaining divergence from stock tclsh and from the sibling 'script' subcommand in the issue list below is either fixed or explicitly recorded as intended, the piperepl patch residue items have decisions (batched into at most one patch rev + suite rebuild), and the console-entry paths have a test-coverage decision.
Acceptance: Every item in the "Issue list" section carries a recorded user decision (fix / document-as-intended / wontfix) and the decided fixes and documentation have landed; the tclsh subcommand's user-facing contract is documented in a durable doc location (not TEMP_REFERENCE); tclshcmd.test covers the decided pipe-testable behaviours; and the dorepl/console-reopen path either has a console-driven test or a recorded decision that G-106-style coverage is not required.
## Context
Origin: PIPEREPL testing after G-103 (achieved - see goals/archive/G-103-runtime-kit-family.md; user session notes in machine-local TEMP_REFERENCE/piperepl_info.txt) plus the 2026-07-23 agent review of the tclsh dispatch in punk_main.tcl and the tclMain_piperepl_905.c patch. The review deduced and pinned the current contract: src/tests/shell/testsuites/punkexe/tclshcmd.test covers the pipe-testable behaviours, and src/buildsuites/suite_tcl90/patches/README.md carries the corrected patch-side semantics (tcl_interactive early-link divergence, reopen-message residue).
Already landed 2026-07-23, prior to this goal (not part of its acceptance): stock leading-dash argument handling ('tclsh - args...' keeps all args in ::argv, stock parity verified against native tclsh 9.0.3), '-encoding name fileName' support (incl. stock fallthrough for incomplete forms), and the lib: refusal pointing at the 'script' subcommand (decision: option (d) - tclsh stays plain, no scriptlib resolution).
The subcommand is an extension of standard tclsh behaviour, not an exact emulator; the guiding principle for the items below is least surprise across punk exe subcommands ('tclsh' vs 'script' vs 'shell').
## Issue list
Each item needs a user decision (fix / document-as-intended / wontfix). Items 1, 2 and 7 need a tclMain piperepl patch rev + suite rebuild and should be batched.
1. Reopen message noise: on the eof-with-dorepl console-reopen path the patch unconditionally prints "? reopen stdin from console" to STDOUT (tclMain_piperepl_905.c). Options: drop / move to stderr / env-gate (e.g TCLSH_PIPEREPL_DEBUG).
2. tcl_interactive read divergence (gate open): a script-arg launch from a CONSOLE reads ::tcl_interactive as 1 where stock tclsh reads 0, because the patch links it to the tty flag before the startup script (the early link is load-bearing - it is how a piped script that sets dorepl opts into prompts/echo). Options: accept + document (current state, recorded in patches/README.md) / patch rev that preserves stock read semantics for script-arg launches while keeping the write lever.
3. argv0 in no-script forms: piped/console no-script runs leave ::argv0 as the kit boot script (e.g //zipfs:/app/main.tcl). Stock tclsh: the exe invocation path; 'script' subcommand stdin form: "-". Decide the value and set it in the no-script branch of the tclsh dispatch.
4. Missing script file: 'tclsh <nonexistent>' dumps the raw errorInfo trace, exposing punk_main lambda internals. The 'script' subcommand prints a clean not-found message and exits 1. Decide whether the tclsh subcommand gets the same file-exists pre-check (source errors from an existing script should keep their full trace - only the not-found case cleans up).
5. Unpatched-runtime console no-arg: 'punk tclsh' with console stdin on a kit whose runtime lacks the piperepl patch blocks in 'read stdin' until ^Z, then evals the typed text. The 'script' subcommand fails fast on terminal stdin with a usage error (terminal probe via chan configure -inputmode/-mode, app-punkscript precedent). Decide fail-fast vs current block-and-eval. Also: the TCLSH_PIPEREPL=0 dispatch path emits leftover debug lines (tcl_interactive value) - clean up.
6. Piped no-arg eval semantics: the dispatch reads stdin to eof then evals the whole buffer - no output until eof on slow pipes, and a mid-stream error aborts the remainder with exit 1. Stock tclsh streams per command-complete chunk, continues after errors, and exits 0. Current behaviour is 'script'-subcommand-coherent (honest exit codes); decide document-as-intended-extension vs emulating stock streaming.
7. ::tclsh(reopened) is C-internal and unpublished - Tcl code can only infer that the console reopen happened via 'info exists ::tclsh(inputbuffer)'. Consider publishing it in the same patch rev as item 1.
8. ::tclsh(istty) is read-only by convention only: a script write corrupts the Tcl-visible launch fact (C behaviour is unaffected - the variable is deliberately unlinked). Options: document-only / a readonly variable trace installed by the dispatch.
9. project_main.tcl parity: mirror the punk_main.tcl tclsh dispatch (piperepl no-arg branch, leading-dash/-encoding forms, lib: refusal) into project_main.tcl and decide how the project-layout copies under src/project_layouts pick it up, so generated-project kits carry the same contract.
10. Documentation home: the tclsh subcommand contract (and the launch subcommand family generally: tclsh/script/shell/punk/shellspy) needs durable user-facing documentation; punk::args definitions are the intended doc source of truth. TEMP_REFERENCE/piperepl_info.txt is machine-local source material only.
11. Console-path test coverage: the dorepl/console-reopen path and console repl entry are deliberately untested by the piped harness (tclshcmd.test hang rule: nothing may set ::tclsh(dorepl)). Decide whether to build a G-106-style hidden-console driven test (AttachConsole + WriteConsoleInput) for these paths.
## Notes
- Goal text is an agent draft persisted at the user's request 2026-07-23; the user intends to provide input before activation - contract wording may be refined then.
- Behaviour references for the review: patches/README.md (patch-side contract incl. gate policy and known divergences), src/tests/shell/AGENTS.md tclshcmd.test bullet (pinned pipe-testable contract), stock comparisons verified against native tclsh 9.0.3 (2026-07-23: leading-dash keeps all args in ::argv with argv0 = exe path; script-arg runs read tcl_interactive 0).
- Patched-runtime kits in bin/ as of 2026-07-23: punk9_beta.exe, punk9bi_beta.exe (punk902z/punk905/punk91 runtimes are unpatched; all carry the current dispatch after the 2026-07-23 rebuild).

2
punkproject.toml

@ -1,4 +1,4 @@
[project] [project]
name = "punkshell" name = "punkshell"
version = "0.18.8" version = "0.19.0"
license = "BSD-2-Clause" license = "BSD-2-Clause"

6
src/buildsuites/suite_tcl90/build.zig.zon

@ -33,10 +33,10 @@
.hash = "N-V-__8AALOqFAA-WUgJ777DabyIuinMYLe_jXido0KjnJbT", .hash = "N-V-__8AALOqFAA-WUgJ777DabyIuinMYLe_jXido0KjnJbT",
.lazy = true, .lazy = true,
}, },
//tclvfs trunk checkout f082c47f9b 2023-09-05 (1.4.2 era) //tclvfs forTcl9 branch checkout 75a2672991 (1.5.0 - the branch BAWT builds ship; tcl9 suites only, user 2026-07-22)
.tclvfs = .{ .tclvfs = .{
.url = "https://core.tcl-lang.org/tclvfs/tarball/f082c47f9b2e04566e8ae70d56c6a02980d43674/tclvfs.tar.gz", .url = "https://core.tcl-lang.org/tclvfs/tarball/75a26729914e2fbc68bf955315d34dc0302e3dbd7e239109785f9f5e30bd5fdd/tclvfs.tar.gz",
.hash = "N-V-__8AAK_2DQDNpjv04LswPFyezzY97rkc5cuwcaqO-Nyq", .hash = "N-V-__8AAHINEQBULsYz_YEtRrrYaPFYdWmEQqvpXW_6Rcl7",
.lazy = true, .lazy = true,
}, },
//tk core-9-0-branch checkout effef6c657 2025-04-07 (9.0.2) //tk core-9-0-branch checkout effef6c657 2025-04-07 (9.0.2)

15
src/buildsuites/suite_tcl90/build905.zig

@ -2040,7 +2040,8 @@ pub fn build(b: *std.Build) !void {
//_ = try tclvfs_static.vfsadd_tclvfs_files(tcl_source_folder, "../tclvfs", b, make_vfs_root); //_ = try tclvfs_static.vfsadd_tclvfs_files(tcl_source_folder, "../tclvfs", b, make_vfs_root);
_ = try tclvfs_shared.vfsadd_tclvfs_files(tcl_source_folder, "../tclvfs", b, make_vfs_root); _ = try tclvfs_shared.vfsadd_tclvfs_files(tcl_source_folder, "../tclvfs", b, make_vfs_root);
//hack //hack
_ = make_vfs_root.addCopyFile(tclvfs_compile.getEmittedBin(), "base/lib/vfs1.4.2/tcl9vfs142.dll"); const tclvfs_info = tclvfs_shared.versionInfo(b, "../tclvfs");
_ = make_vfs_root.addCopyFile(tclvfs_compile.getEmittedBin(), b.fmt("base/lib/vfs{s}/{s}", .{ tclvfs_info.dotversion, tclvfs_info.dll_file }));
// ***************************** // *****************************
//vqtcl/vlerq zip payload removed with the rest of the vqtcl wiring (user 2026-07-20; //vqtcl/vlerq zip payload removed with the rest of the vqtcl wiring (user 2026-07-20;
@ -2383,7 +2384,7 @@ pub fn build(b: *std.Build) !void {
//package-search hook for the lib/ tree //package-search hook for the lib/ tree
_ = famwf.add("base/lib/pkgIndex.tcl", family_libext_pkgindex); _ = famwf.add("base/lib/pkgIndex.tcl", family_libext_pkgindex);
//batteries (installed shape) //batteries (installed shape)
try tclvfs_shared.familyadd_tclvfs_package("../tclvfs", b, famwf, "base/lib/vfs" ++ tclvfs_shared.dotversion, tclvfs_compile); try tclvfs_shared.familyadd_tclvfs_package("../tclvfs", b, famwf, b.fmt("base/lib/vfs{s}", .{tclvfs_info.dotversion}), tclvfs_compile);
_ = famwf.addCopyFile(thread_build.pkgidx, b.fmt("base/lib/thread{s}/pkgIndex.tcl", .{thread_build.version})); _ = famwf.addCopyFile(thread_build.pkgidx, b.fmt("base/lib/thread{s}/pkgIndex.tcl", .{thread_build.version}));
_ = famwf.addCopyFile(thread_build.lib.getEmittedBin(), b.fmt("base/bin/{s}", .{thread_build.dll_file})); _ = famwf.addCopyFile(thread_build.lib.getEmittedBin(), b.fmt("base/bin/{s}", .{thread_build.dll_file}));
_ = famwf.addCopyDirectory(tcllib_out, b.fmt("base/lib/tcllib{s}", .{tcllib_ver}), .{}); _ = famwf.addCopyDirectory(tcllib_out, b.fmt("base/lib/tcllib{s}", .{tcllib_ver}), .{});
@ -2436,7 +2437,7 @@ pub fn build(b: *std.Build) !void {
check.has_side_effects = true; check.has_side_effects = true;
check.addFileArg(b.path("tools/family_check.tcl")); check.addFileArg(b.path("tools/family_check.tcl"));
check.addArgs(&.{ "-exe", installed_path, "-variant", fk.variant, "-expectpatch", tcl_h_patchlevel }); check.addArgs(&.{ "-exe", installed_path, "-variant", fk.variant, "-expectpatch", tcl_h_patchlevel });
check.addArgs(&.{ "-thread", thread_build.version, "-vfs", tclvfs_shared.dotversion, "-tcllib", tcllib_ver }); check.addArgs(&.{ "-thread", thread_build.version, "-vfs", tclvfs_info.dotversion, "-tcllib", tcllib_ver });
if (std.mem.eql(u8, fk.variant, "punk-bi")) { if (std.mem.eql(u8, fk.variant, "punk-bi")) {
check.addArgs(&.{ "-tk", tkb.patchlevel, "-tklib", tklib_ver }); check.addArgs(&.{ "-tk", tkb.patchlevel, "-tklib", tklib_ver });
} }
@ -2451,7 +2452,11 @@ pub fn build(b: *std.Build) !void {
//family runtime executes real tooling from its attached batteries. //family runtime executes real tooling from its attached batteries.
//Depends on the checks: only verified kits get artifact records. //Depends on the checks: only verified kits get artifact records.
//Publication to the real punkbin repo stays a deliberate user step. //Publication to the real punkbin repo stays a deliberate user step.
const familyrev = b.option(u32, "familyrev", "assembly revision N for the -r<N> family artifact names (kit-family-artifacts; default 1)") orelse 1; //default 2: r1 of every (9.0.5, variant) pair was PUBLISHED to punkbin
//2026-07-22 and is immutable - subsequent assemblies (first content
//change: tclvfs forTcl9/1.5.0) emit as r2 until r2 is itself published
//and superseded. Bump this default whenever the current rev publishes.
const familyrev = b.option(u32, "familyrev", "assembly revision N for the -r<N> family artifact names (kit-family-artifacts; default 2 - r1 published 2026-07-22)") orelse 2;
const family_artifacts_step = b.step("kit-family-artifacts", "emit punkbin-layout artifact copies (-r<N>) + toml metadata + sha1sums for the verified family kits -> <prefix>/family/punkbin/<target>"); const family_artifacts_step = b.step("kit-family-artifacts", "emit punkbin-layout artifact copies (-r<N>) + toml metadata + sha1sums for the verified family kits -> <prefix>/family/punkbin/<target>");
const artifacts_outdir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "family", "punkbin", "win32-x86_64" }), "\\", "/"); const artifacts_outdir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "family", "punkbin", "win32-x86_64" }), "\\", "/");
const uuid_tcl = common.manifestUuid(b, tcl_source_folder); const uuid_tcl = common.manifestUuid(b, tcl_source_folder);
@ -2466,7 +2471,7 @@ pub fn build(b: *std.Build) !void {
emit.addFileArg(b.path("tools/family_artifacts.tcl")); emit.addFileArg(b.path("tools/family_artifacts.tcl"));
emit.addArgs(&.{ "-outdir", artifacts_outdir, "-rev", b.fmt("{d}", .{familyrev}), "-target", "win32-x86_64" }); emit.addArgs(&.{ "-outdir", artifacts_outdir, "-rev", b.fmt("{d}", .{familyrev}), "-target", "win32-x86_64" });
emit.addArgs(&.{ "-suite", "suite_tcl90", "-tclpatch", tcl_h_patchlevel, "-zig", builtin.zig_version_string, "-optimize", @tagName(optimize) }); emit.addArgs(&.{ "-suite", "suite_tcl90", "-tclpatch", tcl_h_patchlevel, "-zig", builtin.zig_version_string, "-optimize", @tagName(optimize) });
emit.addArgs(&.{ "-components", b.fmt("Thread {s} vfs {s} tcllib {s} tcllibc {s}", .{ thread_build.version, tclvfs_shared.dotversion, tcllib_ver, tcllib_ver }) }); emit.addArgs(&.{ "-components", b.fmt("Thread {s} vfs {s} tcllib {s} tcllibc {s}", .{ thread_build.version, tclvfs_info.dotversion, tcllib_ver, tcllib_ver }) });
emit.addArgs(&.{ "-bicomponents", b.fmt("Tk {s} tklib {s}", .{ tkb.patchlevel, tklib_ver }) }); emit.addArgs(&.{ "-bicomponents", b.fmt("Tk {s} tklib {s}", .{ tkb.patchlevel, tklib_ver }) });
emit.addArgs(&.{ "-provenance", b.fmt("tcl {s} thread {s} tclvfs {s} tk {s} tcllib {s} tklib {s}", .{ uuid_tcl, uuid_thread, uuid_tclvfs, uuid_tk, uuid_tcllib, uuid_tklib }) }); emit.addArgs(&.{ "-provenance", b.fmt("tcl {s} thread {s} tclvfs {s} tk {s} tcllib {s} tklib {s}", .{ uuid_tcl, uuid_thread, uuid_tclvfs, uuid_tk, uuid_tcllib, uuid_tklib }) });
emit.addArgs(&.{ "-testreports", testreports_dir }); emit.addArgs(&.{ "-testreports", testreports_dir });

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

@ -2,19 +2,44 @@ const std = @import("std");
const fs = std.fs; const fs = std.fs;
const common = @import("../build_common.zig"); const common = @import("../build_common.zig");
const tclvfs_dotversion = "1.4.2"; //G-107-style version derivation (2026-07-22): all version-derived tclvfs names
const tclvfs_nodotversion = "142"; //(package dir, dll names, rc fields, configured vfs.tcl/pkgIndex.tcl contents)
pub const dotversion = tclvfs_dotversion; //follow the CHECKOUT's AC_INIT - the previously hardcoded 1.4.2 stamped
pub const dll_file = "tcl9vfs" ++ tclvfs_nodotversion ++ ".dll"; //mismatched sources once the tclvfs ref moved to the forTcl9 branch (1.5.0),
//the same failure class as the thread 3.0.1/3.0.7 case G-107 fixed.
pub const TclvfsInfo = struct {
dotversion: []const u8, //e.g "1.5.0"
nodots: []const u8, //e.g "150"
dll_file: []const u8, //e.g "tcl9vfs150.dll"
dll8_file: []const u8, //e.g "tclvfs150.dll"
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("tcl9vfs{s}.dll", .{nodots}),
.dll8_file = b.fmt("tclvfs{s}.dll", .{nodots}),
.major = b.dupe(major),
.minor = b.dupe(minor),
};
}
//configure-products (vfs.tcl, pkgIndex.tcl) generated from their .in templates at //configure-products (vfs.tcl, pkgIndex.tcl) generated from their .in templates at
//configure time. Shared by the prefix install and the G-103 family staging so the //configure time. Shared by the prefix install and the G-103 family staging so the
//package is generated from one derivation. //package is generated from one derivation.
fn configuredContent(b: *std.Build, comptime subdir: []const u8, comptime template: []const u8) []const u8 { 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); var t: []const u8 = common.readSourceFile(b, subdir ++ "/" ++ template);
t = common.replaceAll(b, t, "@PACKAGE_VERSION@", tclvfs_dotversion); t = common.replaceAll(b, t, "@PACKAGE_VERSION@", info.dotversion);
t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", dll_file); t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", info.dll_file);
t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", "tclvfs" ++ tclvfs_nodotversion ++ ".dll"); t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", info.dll8_file);
return t; return t;
} }
@ -23,7 +48,8 @@ fn configuredContent(b: *std.Build, comptime subdir: []const u8, comptime templa
//dll alongside (vfs.tcl loads it from its own dir via ::vfs::self). Formerly a //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. //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 { 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 info = versionInfo(b, subdir);
const pkgsub = b.fmt("lib/vfs{s}", .{info.dotversion});
const install_scripts = b.addInstallDirectory(.{ const install_scripts = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library"), .source_dir = b.path(subdir ++ "/library"),
.install_dir = .prefix, .install_dir = .prefix,
@ -35,7 +61,7 @@ pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs
const install_template = b.addInstallDirectory(.{ const install_template = b.addInstallDirectory(.{
.source_dir = b.path(subdir ++ "/library/template"), .source_dir = b.path(subdir ++ "/library/template"),
.install_dir = .prefix, .install_dir = .prefix,
.install_subdir = pkgsub ++ "/template", .install_subdir = b.fmt("{s}/template", .{pkgsub}),
}); });
b.getInstallStep().dependOn(&install_template.step); b.getInstallStep().dependOn(&install_template.step);
const wf = b.addWriteFiles(); const wf = b.addWriteFiles();
@ -43,57 +69,57 @@ pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs
.{ "library/vfs.tcl.in", "vfs.tcl" }, .{ "library/vfs.tcl.in", "vfs.tcl" },
.{ "pkgIndex.tcl.in", "pkgIndex.tcl" }, .{ "pkgIndex.tcl.in", "pkgIndex.tcl" },
}) |pair| { }) |pair| {
const gen = wf.add(pair[1], configuredContent(b, subdir, pair[0])); const gen = wf.add(pair[1], configuredContent(b, subdir, pair[0], info));
const inst = b.addInstallFileWithDir(gen, .prefix, pkgsub ++ "/" ++ pair[1]); const inst = b.addInstallFileWithDir(gen, .prefix, b.fmt("{s}/{s}", .{ pkgsub, pair[1] }));
b.getInstallStep().dependOn(&inst.step); b.getInstallStep().dependOn(&inst.step);
} }
const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, pkgsub ++ "/" ++ dll_file); const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, b.fmt("{s}/{s}", .{ pkgsub, info.dll_file }));
b.getInstallStep().dependOn(&dll_inst.step); b.getInstallStep().dependOn(&dll_inst.step);
} }
//G-103: write the SAME installed-shape vfs package into a family staging tree //G-103: write the SAME installed-shape vfs package into a family staging tree
//(WriteFiles) at destsub (e.g "base/lib/vfs1.4.2") - scripts, template dir, //(WriteFiles) at destsub (e.g "base/lib/vfs1.5.0") - scripts, template dir,
//generated vfs.tcl/pkgIndex.tcl, dll inside the package dir. //generated vfs.tcl/pkgIndex.tcl, dll inside the package dir.
pub fn familyadd_tclvfs_package(comptime subdir: []const u8, b: *std.Build, wf: *std.Build.Step.WriteFile, comptime destsub: []const u8, tclvfs_compile: *std.Build.Step.Compile) !void { pub fn familyadd_tclvfs_package(comptime subdir: []const u8, b: *std.Build, wf: *std.Build.Step.WriteFile, destsub: []const u8, tclvfs_compile: *std.Build.Step.Compile) !void {
const info = versionInfo(b, subdir);
_ = wf.addCopyDirectory(b.path(subdir ++ "/library"), destsub, .{ .include_extensions = &.{".tcl"} }); _ = wf.addCopyDirectory(b.path(subdir ++ "/library"), destsub, .{ .include_extensions = &.{".tcl"} });
_ = wf.addCopyDirectory(b.path(subdir ++ "/library/template"), destsub ++ "/template", .{}); _ = wf.addCopyDirectory(b.path(subdir ++ "/library/template"), b.fmt("{s}/template", .{destsub}), .{});
_ = wf.add(destsub ++ "/vfs.tcl", configuredContent(b, subdir, "library/vfs.tcl.in")); _ = wf.add(b.fmt("{s}/vfs.tcl", .{destsub}), configuredContent(b, subdir, "library/vfs.tcl.in", info));
_ = wf.add(destsub ++ "/pkgIndex.tcl", configuredContent(b, subdir, "pkgIndex.tcl.in")); _ = wf.add(b.fmt("{s}/pkgIndex.tcl", .{destsub}), configuredContent(b, subdir, "pkgIndex.tcl.in", info));
_ = wf.addCopyFile(tclvfs_compile.getEmittedBin(), destsub ++ "/" ++ dll_file); _ = wf.addCopyFile(tclvfs_compile.getEmittedBin(), b.fmt("{s}/{s}", .{ destsub, info.dll_file }));
} }
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 { 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; //recursive copy of the script library into the szip staging tree (the
//const version = "1.4.2"; //generated configure-products + dll are added by the caller/familyadd paths)
//recursive copy const info = versionInfo(b, tcldir ++ "/" ++ subdir);
//_ = make_vfs_root.addCopyDirectory(b.path(tcl_source_folder ++ "/../tclvfs/library"), "base/lib/vfs1.4.2", .{}); _ = vfswrite.addCopyDirectory(b.path(tcldir ++ "/" ++ subdir ++ "/library"), b.fmt("base/lib/vfs{s}", .{info.dotversion}), .{});
_ = vfswrite.addCopyDirectory(b.path(tcldir ++ "/" ++ subdir ++ "/library"), b.pathJoin(&.{ "base", "lib", "vfs" ++ tclvfs_dotversion }), .{});
return vfswrite; return vfswrite;
} }
pub fn build_tclvfs(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 { pub fn build_tclvfs(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 {
//review - libname Vfs vs vfs??? const info = versionInfo(b, subdir);
const lib = b.addLibrary(.{ const lib = b.addLibrary(.{
.linkage = .dynamic, .linkage = .dynamic,
.name = "tcl9vfs142", .name = b.fmt("tcl9vfs{s}", .{info.nodots}),
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}), }),
}); });
//_ = stublib;
var flags = std.array_list.Managed([]const u8).init(b.allocator); var flags = std.array_list.Managed([]const u8).init(b.allocator);
defer flags.deinit(); defer flags.deinit();
//try flags.appendSlice(&.{ "-Wall", "-O3" });
try flags.appendSlice(&.{ "-Wall", "-O3", "-fomit-frame-pointer" }); try flags.appendSlice(&.{ "-Wall", "-O3", "-fomit-frame-pointer" });
try flags.appendSlice(&.{ try flags.appendSlice(&.{
"-DPACKAGE_NAME=\"vfs\"", "-DPACKAGE_NAME=\"vfs\"",
"-DPACKAGE_TARNAME=\"vfs\"", "-DPACKAGE_TARNAME=\"vfs\"",
"-DPACKAGE_VERSION=\"" ++ tclvfs_dotversion ++ "\"", b.fmt("-DPACKAGE_VERSION=\"{s}\"", .{info.dotversion}),
"-DPACKAGE_STRING=\"vfs " ++ tclvfs_dotversion ++ "\"", b.fmt("-DPACKAGE_STRING=\"vfs {s}\"", .{info.dotversion}),
"-DPACKAGE_BUGREPORT=\"\"", "-DPACKAGE_BUGREPORT=\"\"",
"-DPACKAGE_URL=\"\"", "-DPACKAGE_URL=\"\"",
"-DBUILD_VFS=1", //forTcl9 vfs.c guards <sys/stat.h> 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", "-DTCL_CFG_OPTIMIZED=1",
"-DUSE_TCL_STUBS=1", "-DUSE_TCL_STUBS=1",
"-DZIPFS_BUILD=1", "-DZIPFS_BUILD=1",
@ -101,24 +127,39 @@ pub fn build_tclvfs(comptime tcldir: []const u8, comptime subdir: []const u8, b:
}); });
if (target.result.os.tag == .windows) { if (target.result.os.tag == .windows) {
try flags.appendSlice(&.{ try flags.appendSlice(&.{
"-static-libgcc", //forTcl9 export guard is lowercase BUILD_vfs ("should be undefined for
//Unix" per vfs.c) - the trunk-era BUILD_VFS spelling no longer matches.
//(-static-libgcc dropped from the cflags: a link-stage arg clang
//reports unused at compile - zig manages runtime-lib linkage anyway.)
"-DBUILD_vfs=/**/",
}); });
} }
//-DBUILD_VFS only on windows? value?
var sources = std.array_list.Managed([]const u8).init(b.allocator);
try sources.append(subdir ++ "/generic/vfs.c");
lib.root_module.addIncludePath(b.path(subdir)); lib.root_module.addIncludePath(b.path(subdir));
lib.root_module.addIncludePath(b.path(subdir ++ "/generic")); lib.root_module.addIncludePath(b.path(subdir ++ "/generic"));
lib.root_module.addIncludePath(b.path(tcldir ++ "/generic")); lib.root_module.addIncludePath(b.path(tcldir ++ "/generic"));
lib.root_module.addIncludePath(b.path(tcldir ++ "/generic"));
if (target.result.os.tag == .windows) { if (target.result.os.tag == .windows) {
lib.root_module.addIncludePath(b.path(tcldir ++ "/win")); lib.root_module.addIncludePath(b.path(tcldir ++ "/win"));
} else { } else {
lib.root_module.addIncludePath(b.path(tcldir ++ "/unix")); lib.root_module.addIncludePath(b.path(tcldir ++ "/unix"));
} }
lib.root_module.addCSourceFiles(.{ //Include-order shim (overlay TU - source trees never written): under zig's
.files = sources.items, //mingw headers '__stat64' is a MACRO for _stat64 (_mingw_stat64.h, pulled in
//by <sys/stat.h>); tcl.h's 'typedef struct __stat64 Tcl_StatBuf' parsed
//BEFORE that include binds a raw forever-incomplete __stat64 tag and the
//forTcl9 vfs.c's direct Tcl_StatBuf field accesses then fail. Compiling a
//generated TU that includes <sys/stat.h> first makes the macro visible when
//tcl.h parses. (MSVC has a real struct __stat64 tag - upstream never sees
//this. A -include flag would be simpler but trips zig cc's CacheCheckFailed.)
const wf_shim = b.addWriteFiles();
const vfs_shim = wf_shim.add("vfs_statorder_shim.c",
\\/* generated include-order shim - see build_tclvfs_shared.zig */
\\#include <sys/stat.h>
\\#include "vfs.c"
\\
);
lib.root_module.addCSourceFile(.{
.file = vfs_shim,
.flags = flags.items, .flags = flags.items,
}); });
@ -127,16 +168,15 @@ pub fn build_tclvfs(comptime tcldir: []const u8, comptime subdir: []const u8, b:
lib.root_module.addWin32ResourceFile(.{ lib.root_module.addWin32ResourceFile(.{
.file = b.path(subdir ++ "/win/tclvfs.rc"), .file = b.path(subdir ++ "/win/tclvfs.rc"),
.flags = &.{ .flags = &.{
"-DPACKAGE_MAJOR=1", b.fmt("-DPACKAGE_MAJOR={s}", .{info.major}),
"-DPACKAGE_MINOR=4", b.fmt("-DPACKAGE_MINOR={s}", .{info.minor}),
"-DPACKAGE_VERSION=" ++ tclvfs_dotversion, b.fmt("-DPACKAGE_VERSION={s}", .{info.dotversion}),
"-DDOTVERSION=" ++ tclvfs_dotversion, b.fmt("-DDOTVERSION={s}", .{info.dotversion}),
"-DCOMMAVERSION=" ++ tclvfs_dotversion, b.fmt("-DCOMMAVERSION={s}", .{info.dotversion}),
"-DVERSION=" ++ tclvfs_nodotversion, b.fmt("-DVERSION={s}", .{info.nodots}),
}, },
}); //ignored for non-windows targets }); //ignored for non-windows targets
// COMMAVERSION??? VERSION???
b.installArtifact(lib); b.installArtifact(lib);
return lib; //static library return lib;
} }

31
src/buildsuites/suite_tcl90/expected_test_failures_tclvfs.txt

@ -3,23 +3,14 @@
# step (policy gate) fails on any failed test NOT listed here, and notes listed # step (policy gate) fails on any failed test NOT listed here, and notes listed
# tests that no longer fail (candidates for removal). # tests that no longer fail (candidates for removal).
# #
# Baseline established 2026-07-21 against tclvfs trunk (vfs 1.4.2), suite-built # Baseline re-established 2026-07-22 against the tclvfs forTcl9 branch
# tclsh90s.exe 9.0.5, machine superbee (windows 11). Census totals at capture: # (vfs 1.5.0, checkout 75a2672991), suite-built tclsh90s.exe 9.0.5, machine
# 51 run / 39 passed / 5 skipped / 7 failed, all dispositioned below. # superbee (windows 11). Census totals at capture: 51 run / 42 passed /
# 9 skipped / 0 failed - the branch move CLEARED the whole 1.4.2-era baseline:
# -- tcl9 glob-nomatch semantics through vfs handlers: glob on a non-matching # the tcl9 glob-nomatch drift entries (vfs-4.2, vfsZip-1.6/1.7) now pass (the
# pattern / non-present directory inside a mounted vfs returns an empty result # forTcl9 branch reconciles tcl9 vfs-handler glob semantics), and the
# where the tests (written for tcl8-era vfs) expect the "no files matched" # network-reaching vfsFtp-1.1/1.2/1.3 + vfsUrl-2.1 entries now SKIP rather
# error. Consistent behaviour drift, not a build defect; candidate upstream # than fail under this branch's suite. Prior 1.4.2-era baseline + dispositions:
# test/behaviour reconciliation. # git history of this file (2026-07-21 capture).
vfs-4.2 #
vfsZip-1.6 # (no expected failures)
vfsZip-1.7
# -- network-reaching tests (the suite census runs offline-tolerant): remote
# FTP hosts (ftp.ucsd.edu, ftp.tcl.tk) unreachable/retired. The goal survey
# (G-107) anticipated skipping/dispositioning vfsFtp/vfsUrl.
vfsFtp-1.1
vfsFtp-1.2
vfsFtp-1.3
vfsUrl-2.1

13
src/buildsuites/suite_tcl90/patches/README.md

@ -44,9 +44,16 @@ stock-behaviour escape hatches are `TCLSH_PIPEREPL=0` and the unpatched plain
products. Era files (`tclMain_piperepl.c`, the `.patch`) are kept unmodified as the products. Era files (`tclMain_piperepl.c`, the `.patch`) are kept unmodified as the
recovered 2024 artifacts. Known divergence when the gate is OPEN: `tcl_interactive` recovered 2024 artifacts. Known divergence when the gate is OPEN: `tcl_interactive`
is linked to the internal tty flag BEFORE the startup script runs (stock links it is linked to the internal tty flag BEFORE the startup script runs (stock links it
only when entering the repl loop), so a script that WRITES ::tcl_interactive only when entering the repl loop). Two visible effects: a script that WRITES
influences prompting behaviour in the patched shell where stock would ignore the ::tcl_interactive influences prompting behaviour in the patched shell where stock
write until repl entry - scripts that merely read it see identical values. Note the would ignore the write until repl entry (load-bearing - it is how a piped script
that sets dorepl opts into prompts/echo in the reopened repl); and a script-arg
launch from a CONSOLE READS 1 where stock reads 0 (stock sets it once to
`!path && istty`; the early link overwrites that with the tty flag. Piped launches
read 0 either way, so only console script runs diverge for readers - verified
against punk9_beta 2026-07-23). Known residue: on the eof-with-dorepl path the
patch unconditionally prints "? reopen stdin from console" to STDOUT (debug-era
message); quieting/stderr/env-gating it needs a patch rev + suite rebuild. Note the
punkshell-level analogue for the punk repl is G-038 (piped-to-interactive session punkshell-level analogue for the punk repl is G-038 (piped-to-interactive session
continuity) - this patch is the plain-tclsh/kit-runtime counterpart. continuity) - this patch is the plain-tclsh/kit-runtime counterpart.

6
src/buildsuites/suite_tcl90/sources.config

@ -43,7 +43,11 @@
source tcl fossil https://core.tcl-lang.org/tcl core-9-0-branch tcl905 source tcl fossil https://core.tcl-lang.org/tcl core-9-0-branch tcl905
source tclthread fossil https://core.tcl-lang.org/thread trunk tclthread source tclthread fossil https://core.tcl-lang.org/thread trunk tclthread
source tclvfs fossil https://core.tcl-lang.org/tclvfs trunk tclvfs # tclvfs forTcl9 branch (user 2026-07-22): trunk is the 1.4.2 era; forTcl9 carries
# 1.5.0 (the branch BAWT builds ship). TCL 9 SUITES ONLY - a future suite_tcl86
# (G-099) keeps trunk. Version-derived names follow the checkout's AC_INIT
# (build_tclvfs_shared.zig versionInfo - the G-107 derivation pattern).
source tclvfs fossil https://core.tcl-lang.org/tclvfs forTcl9 tclvfs
# G-098: Tk + companion script libraries. tklib is pure-tcl (installed by its own # G-098: Tk + companion script libraries. tklib is pure-tcl (installed by its own
# installer where that matters); tcllib's accelerators (tcllibc) are built via critcl. # installer where that matters); tcllib's accelerators (tcllibc) are built via critcl.

38
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd vendored

@ -1747,8 +1747,19 @@ case "$action" in
echo "server default for $archtail: $platform_default" echo "server default for $archtail: $platform_default"
fi fi
if [[ -n "$activename" ]]; then if [[ -n "$activename" ]]; then
#the default names an immutable -r<N> ARTIFACT while the active is
#typically its materialized WORKING name - the beside-toml's 'name'
#field records which artifact the working copy came from, so match
#on either identity
active_artifact=""
activetoml="$archdir/$(rootname_of "$activename").toml"
if [[ -f "$activetoml" ]]; then
active_artifact=$(sed -n 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$activetoml" | head -n 1)
fi
matchnote="" matchnote=""
[[ -n "$platform_default" && "$activename" == "$platform_default" ]] && matchnote=" (= server default)" if [[ -n "$platform_default" ]] && [[ "$activename" == "$platform_default" || "$active_artifact" == "$platform_default" ]]; then
matchnote=" (= server default)"
fi
echo "active (local): $activename$matchnote" echo "active (local): $activename$matchnote"
fi fi
echo "-----------------------------------------------------------------------" echo "-----------------------------------------------------------------------"
@ -1775,6 +1786,12 @@ case "$action" in
#mode but bash read does not - without this a locally-present runtime #mode but bash read does not - without this a locally-present runtime
#also shows as a remote-only row (\r-suffixed name fails the -f test) #also shows as a remote-only row (\r-suffixed name fails the -f test)
tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do
#skip support files riding in the server's sha1sums beside the
#runtimes (artifact metadata tomls etc) - same extension set the
#local candidate filter excludes
case "$rname" in
*.txt|*.toml|*.tm|*.tmp|*.log) continue;;
esac
if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then
annot="" annot=""
[[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)" [[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)"
@ -3063,8 +3080,20 @@ function psmain {
Write-host "server default for ${arch}: $platform_default" Write-host "server default for ${arch}: $platform_default"
} }
if ($activename -ne "") { if ($activename -ne "") {
#the default names an immutable -r<N> ARTIFACT while the active
#is typically its materialized WORKING name - the beside-toml's
#'name' field records which artifact the working copy came from,
#so match on either identity
$active_artifact = ""
$activetoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $activename) + ".toml")
if (Test-Path -Path $activetoml -PathType Leaf) {
foreach ($tline in (Get-Content -Path $activetoml)) {
$m = [regex]::Match($tline, '^\s*name\s*=\s*"(.*)"\s*$')
if ($m.Success) { $active_artifact = $m.Groups[1].Value; break }
}
}
$matchnote = "" $matchnote = ""
if ($platform_default -ne "" -and $activename -eq $platform_default) { if ($platform_default -ne "" -and ($activename -eq $platform_default -or $active_artifact -eq $platform_default)) {
$matchnote = " (= server default)" $matchnote = " (= server default)"
} }
Write-host "active (local): $activename$matchnote" Write-host "active (local): $activename$matchnote"
@ -3113,6 +3142,11 @@ function psmain {
[array]::Sort($remotekeys, [System.StringComparer]::Ordinal) [array]::Sort($remotekeys, [System.StringComparer]::Ordinal)
foreach ($key in $remotekeys) { foreach ($key in $remotekeys) {
if (-not ($localdict.ContainsKey($key))) { if (-not ($localdict.ContainsKey($key))) {
#skip support files riding in the server's sha1sums beside
#the runtimes (artifact metadata tomls etc) - same extension
#set the local candidate filter excludes
$rext = [System.IO.Path]::GetExtension($key)
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
$annot = "" $annot = ""
if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" }
write-host -nonewline " $lhs_missing" write-host -nonewline " $lhs_missing"

14
src/modules/punk/mod-999999.0a1.0.tm

@ -71,12 +71,16 @@ namespace eval punk::mod::cli {
} }
} }
dict set appinfo versions $versions dict set appinfo versions $versions
#todo - natsort! #version-aware selection (2026-07-22 audit): was a plain lexical lsort
set sorted_versions [lsort $versions] #(1.10 sorts before 1.9) with lindex 0 - the LOWEST - as 'latest'.
set latest [lindex $sorted_versions 0] #Empty version (unversioned main) is excluded from comparison (package
if {$latest eq "" && [llength $sorted_versions] > 1} { #vcompare errors on empty operands); latest = highest by vcompare.
set latest [lindex $sorted_versions 1] set realversions {}
foreach v $versions {
if {$v ne ""} {lappend realversions $v}
} }
set sorted_versions [lsort -command {package vcompare} $realversions]
set latest [lindex $sorted_versions end] ;#"" when only an unversioned main exists
dict set appinfo latest $latest dict set appinfo latest $latest
dict set appinfo bases $bases dict set appinfo bases $bases

3
src/modules/punk/mod-buildversion.txt

@ -1,3 +1,4 @@
0.1.1 0.1.1
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.1.2 - punk::apps appinfo 'latest' selection fixed (2026-07-22 version-selection audit): was a plain lexical lsort (1.10 before 1.9) taking lindex 0 - the LOWEST - as latest; now lsort -command {package vcompare} taking the highest, with empty (unversioned-main) entries excluded from comparison (vcompare errors on empty operands).

47
src/scriptapps/bin/punk-runtime.ps1

@ -199,6 +199,41 @@ function Get-PunkRuntimeRootName {
} }
return $name return $name
} }
#Dependency-free SHA1 (robustness fix 2026-07-23): Get-FileHash is a SCRIPT-defined
#function in Windows PowerShell 5.1's Microsoft.PowerShell.Utility, resolved via
#PSModulePath autoloading - on machines with a damaged PSModulePath (pwsh7 module-path
#pollution, overridden env vars, redirected user module dirs) it vanishes while the
#module's COMPILED cmdlets (Write-Host, Invoke-WebRequest) still work, observed in the
#field ("Get-FileHash ... not recognized" under PS 5). Hashing here goes through .NET
#directly so fetch verification and list -remote comparison work on any PowerShell
#edition/state, with a one-time informational note when the damage is present.
#Returns lowercase hex (sha1sums.txt convention; -eq/-ne comparisons are
#case-insensitive so case never matters for verification).
$script:punkFileHashNoteShown = $false
function Get-PunkFileSha1 {
param([string] $Path)
if (-not $script:punkFileHashNoteShown) {
$script:punkFileHashNoteShown = $true
if ($PSVersionTable.PSVersion.Major -le 5 -and -not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {
Write-Host "note: Get-FileHash is unavailable in this Windows PowerShell (PSModulePath on this machine may be damaged) - punk-runtime uses built-in .NET hashing and is unaffected"
}
}
#Resolve-Path: .NET's current directory is not PowerShell's location, so hand
#[System.IO.File] an absolute provider path
$fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath
$sha1 = [System.Security.Cryptography.SHA1]::Create()
try {
$fs = [System.IO.File]::OpenRead($fullpath)
try {
$hashbytes = $sha1.ComputeHash($fs)
} finally {
$fs.Dispose()
}
} finally {
$sha1.Dispose()
}
return (($hashbytes | ForEach-Object { $_.ToString("x2") }) -join "")
}
#G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted #G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted
#by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a #by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a
#short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent.
@ -473,8 +508,8 @@ function psmain {
if (Test-Path -Path $output -PathType Leaf) { if (Test-Path -Path $output -PathType Leaf) {
Write-Host "Runtime already found at $output" Write-Host "Runtime already found at $output"
Write-Host "Checking sha1 checksum of local file versus sha1 of server file" Write-Host "Checking sha1 checksum of local file versus sha1 of server file"
$file_sha1 = Get-FileHash -Path "$output" -Algorithm SHA1 $file_sha1 = Get-PunkFileSha1 -Path "$output"
if (${file_sha1}.Hash -ne $stored_sha1) { if ($file_sha1 -ne $stored_sha1) {
Write-Host "$runtime on server has different sha1 hash - Download required" Write-Host "$runtime on server has different sha1 hash - Download required"
$need_download = $true $need_download = $true
} }
@ -498,8 +533,8 @@ function psmain {
} }
Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt" Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt"
Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?) Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?)
$newfile_sha1 = Get-FileHash -Path "${output}.tmp" -Algorithm SHA1 $newfile_sha1 = Get-PunkFileSha1 -Path "${output}.tmp"
if (${newfile_sha1}.Hash -eq $stored_sha1) { if ($newfile_sha1 -eq $stored_sha1) {
Write-Host "sha1 checksum ok" Write-Host "sha1 checksum ok"
Move-Item -Path "${output}.tmp" -Destination "${output}" -Force Move-Item -Path "${output}.tmp" -Destination "${output}" -Force
Write-Host "Runtime is available at ${output}" Write-Host "Runtime is available at ${output}"
@ -731,8 +766,8 @@ function psmain {
#local runtimes in the comparison #local runtimes in the comparison
$dircontents = Get-PunkRuntimeCandidates $archfolder $dircontents = Get-PunkRuntimeCandidates $archfolder
foreach ($f in $dircontents) { foreach ($f in $dircontents) {
$local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1 $local_sha1 = Get-PunkFileSha1 -Path $(${f}.FullName)
$localdict[$f.Name] = ${local_sha1}.Hash $localdict[$f.Name] = $local_sha1
} }
} }

3
src/tests/shell/AGENTS.md

@ -20,7 +20,8 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038. - `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk. - `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.
- punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate -confirm 0` (unattended runs must pass the flag - the REPLACE confirmation aborts fast on non-interactive stdin instead of reading a piped `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`. - punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate -confirm 0` (unattended runs must pass the flag - the REPLACE confirmation aborts fast on non-interactive stdin instead of reading a piped `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the runtime.cmd unix payload (`src/scriptapps/bin/runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run. - `testsuites/punkexe/tclshcmd.test` covers the `tclsh` subcommand (punk_main.tcl dispatch): script-file sourcing (argv0/argv/info-script state, honest exit codes incl. explicit exit), piped no-arg stdin evaluation, stock argument forms (leading `-` arg = no script, all args stay in `::argv`; `-encoding name file` sources with the named encoding, incomplete `-encoding` forms fall through to argv; verified byte-level via a utf-8/iso8859-1 fixture), the `lib:` refusal (plain-tclsh semantics - pointer to the `script` subcommand, exit 1), and - on kits built on a TCLSH_PIPEREPL-patched runtime (G-096/G-103, see `src/buildsuites/suite_tcl90/patches/README.md`) - the piperepl launch-state contract: ::tclsh(istty) published, piped script-arg launch state all-zero, unconsumed piped input NOT evaluated unless the script sets `::tclsh(evalinput) 1`, and the script's right to consume stdin itself; plus the degraded-mode notice on unpatched runtimes. Patched-kit resolution: `env(PUNK_PIPEREPL_TEST_EXE)` if it probes as patched, else the punkexe if patched, else `bin/punk9_beta.exe`/`bin/punk9bi_beta.exe`. HANG RULE: no test or fixture may set `::tclsh(dorepl)` - that path reopens stdin from the console (CONIN$) and blocks under this piped harness; console-reopen coverage needs the G-106 hidden-console recipe and is deliberately absent.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the punk-runtime.cmd unix payload (`src/scriptapps/bin/punk-runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run.
- `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes. - `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes.
- GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build. - GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build.
- The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset. - The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset.

3
src/tests/shell/testsuites/binscripts/runtimebash_wsl.test

@ -96,7 +96,8 @@ namespace eval ::testspace {
set r [punktest::wsl::run_in $::testspace::stagingdir {cat runtime/linux-x86_64/active.toml}] set r [punktest::wsl::run_in $::testspace::stagingdir {cat runtime/linux-x86_64/active.toml}]
lappend result [string trim [dict get $r output]] lappend result [string trim [dict get $r output]]
set r [rtbash list] set r [rtbash list]
lappend result [string match {*\* fakert_beta (active)*} [dict get $r output]] #row is column-padded since the G-103 metadata era: '* <name %-35s> (active) <metadata>'
lappend result [string match {*\* fakert_beta*(active)*} [dict get $r output]]
set r [rtbash run hello world] set r [rtbash run hello world]
lappend result [dict get $r ok] lappend result [dict get $r ok]
lappend result [string match "*FAKERT_BETA_RAN hello world*" [dict get $r output]] lappend result [string match "*FAKERT_BETA_RAN hello world*" [dict get $r output]]

386
src/tests/shell/testsuites/punkexe/tclshcmd.test

@ -0,0 +1,386 @@
package require tcltest
#Tests for the built punk executable's 'tclsh' subcommand (punk_main.tcl dispatch).
#Covers two layers:
# - dispatch contract (any punk kit): script-file sourcing with argv0/argv/info-script
# state, piped-stdin evaluation when no script argument is given, honest exit codes
# (script error, explicit exit code, piped-input error), stock tclsh argument forms
# (leading '-' arg means no script - all args stay in ::argv; '-encoding name file'
# sources with the named encoding; incomplete -encoding forms fall through to argv),
# and the lib: refusal (scriptlib resolution is a punk facility - the subcommand keeps
# plain tclsh semantics and points at the 'script' subcommand, exit 1).
# - piperepl launch-state contract (kits built on a TCLSH_PIPEREPL-patched runtime,
# G-096/G-103 - see src/buildsuites/suite_tcl90/patches/README.md): ::tclsh(istty)
# published as the launch-time stdin-tty fact, script-arg piped launch state
# (istty 0, tcl_interactive 0, dorepl 0, evalinput 0), unconsumed piped input NOT
# evaluated by default (safety default), ::tclsh(evalinput) opt-in evaluating
# leftover input after the script, and the script's right to consume stdin itself.
#
#HANG SAFETY: no test or fixture ever sets ::tclsh(dorepl) - that is the console-reopen
#path (stdin reopened from CONIN$ at eof), which blocks on a real console and cannot be
#driven by this piped harness (needs the hidden-console/WriteConsoleInput recipe, G-106).
#All children here terminate at stdin EOF by construction; punk_run force-kills on
#timeout regardless.
#
#Dispatch tests target the standard punkexe: env(PUNK_SHELL_TEST_EXE), else
#<projectroot>/bin/punk902z.exe, else <projectroot>/bin/punkshell902 (constraint
#punkexeavailable). Piperepl tests target a patched kit: env(PUNK_PIPEREPL_TEST_EXE)
#if it probes as patched, else the punkexe if patched, else the first patched candidate
#of bin/punk9_beta.exe, bin/punk9bi_beta.exe (constraint pipereplkitavailable; probe =
#piped no-arg run checking [info exists ::tclsh(istty)], the G-103 family_check
#discriminator). Constraint pipereplmissing covers the degraded-mode notice on kits
#whose runtime lacks the patch.
#
#NOTE: exercises BUILT executables - after changing the tclsh dispatch in
#src/vfs/_config/punk_main.tcl, rebuild the kit under test ('make.tcl project' for the
#standard punkexe; family kits like punk9_beta come from src/buildsuites wrap runs) or
#these tests run against a stale binary.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot>
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]]
variable punkexe ""
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} {
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)]
} else {
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] {
if {[file exists $candidate]} {
set punkexe $candidate
break
}
}
}
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}]
variable punk_run_timeout_ms 15000
variable runstate
array set runstate {}
proc punk_run_read {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
}
#Run the target executable with args, feed stdin_data via pipe then half-close (EOF).
#Returns dict: timedout 0|1, exitcode <int|kill-info|"">, output <combined stdout+stderr>.
#On timeout the process (and any pipeline members) are forcibly killed so the suite cannot hang.
#No PUNK_PIPE_EOF here - the tclsh subcommand has no eof-policy env; children terminate at
#stdin EOF because nothing in this suite sets ::tclsh(dorepl) (see HANG SAFETY above).
proc punk_run {exe cmdargs stdin_data} {
variable runstate
variable punk_run_timeout_ms
array unset runstate
set runstate(output) ""
set runstate(done) ""
set chan [open |[list $exe {*}$cmdargs 2>@1] r+]
set pids [pid $chan]
chan configure $chan -blocking 0 -translation binary
catch {
puts -nonewline $chan $stdin_data
flush $chan
chan close $chan write ;#half-close - child sees EOF on stdin
}
set timerid [after $punk_run_timeout_ms [list set [namespace current]::runstate(done) timeout]]
chan event $chan readable [list [namespace current]::punk_run_read $chan]
while {$runstate(done) eq ""} {
vwait [namespace current]::runstate(done)
}
after cancel $timerid
chan event $chan readable {}
set timedout [expr {$runstate(done) eq "timeout"}]
set exitcode ""
if {$timedout} {
foreach p $pids {
if {$::tcl_platform(platform) eq "windows"} {
catch {exec {*}[auto_execok taskkill] /F /PID $p}
} else {
catch {exec kill -9 $p}
}
}
catch {close $chan}
} else {
catch {chan configure $chan -blocking 1}
if {[catch {close $chan} errmsg erropts]} {
set ecode [dict get $erropts -errorcode]
switch -- [lindex $ecode 0] {
CHILDSTATUS { set exitcode [lindex $ecode 2] }
default { set exitcode $ecode }
}
} else {
set exitcode 0
}
}
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)]
}
#Classify a kit's tclsh subcommand via a piped no-arg run (terminates at EOF on every
#known dispatch version; never sets dorepl):
# machinery - piperepl-patched runtime, ::tclsh machinery published
# evalok - unpatched runtime, but piped no-arg stdin evaluation works
# noeval - piped stdin was not evaluated (pre-else-branch kit or launch failure)
variable piperepl_probe_script [string cat {puts [list PRPROBE [info exists ::tclsh(istty)]]} \n]
proc piperepl_probe {exe} {
variable piperepl_probe_script
set rd [punk_run $exe [list tclsh] $piperepl_probe_script]
if {[dict get $rd timedout]} {
return noeval
}
if {[string first "PRPROBE 1" [dict get $rd output]] >= 0} {
return machinery
}
if {[string first "PRPROBE 0" [dict get $rd output]] >= 0} {
return evalok
}
return noeval
}
variable punkexe_probe noeval
if {[testConstraint punkexeavailable]} {
set punkexe_probe [piperepl_probe $punkexe]
}
variable pipereplexe ""
if {[info exists ::env(PUNK_PIPEREPL_TEST_EXE)] && $::env(PUNK_PIPEREPL_TEST_EXE) ne ""} {
set cand [file normalize $::env(PUNK_PIPEREPL_TEST_EXE)]
if {[file exists $cand] && [piperepl_probe $cand] eq "machinery"} {
set pipereplexe $cand
}
} elseif {$punkexe_probe eq "machinery"} {
set pipereplexe $punkexe
} else {
foreach cand [list [file join $projectroot bin punk9_beta.exe] [file join $projectroot bin punk9bi_beta.exe]] {
if {[file exists $cand] && [piperepl_probe $cand] eq "machinery"} {
set pipereplexe $cand
break
}
}
}
testConstraint pipereplkitavailable [expr {$pipereplexe ne ""}]
testConstraint pipereplmissing [expr {[testConstraint punkexeavailable] && $punkexe_probe eq "evalok"}]
#fixture scripts (tcltest -tmpdir; forward-slash paths - kit script args need them)
variable fx_dispatch_argv [makeFile {puts [list TARGS argv0tail [file tail $::argv0] argv $::argv scripttail [file tail [info script]] tcli $::tcl_interactive]} dispatch_argv.tcl]
variable fx_dispatch_err [makeFile {puts TCLSHF-BEFORE-ERR
error tclshfile-boom} dispatch_err.tcl]
variable fx_dispatch_exit5 [makeFile {puts TCLSHF-EXIT5-MARK
exit 5} dispatch_exit5.tcl]
variable fx_pr_state [makeFile {puts [list PRSCRIPT istty $::tclsh(istty) tcli $::tcl_interactive dorepl $::tclsh(dorepl) evalinput $::tclsh(evalinput)]} pr_state.tcl]
variable fx_pr_noconsume [makeFile {puts PRNOCON-MARKER} pr_noconsume.tcl]
variable fx_pr_optin [makeFile {puts PROPTIN-MARKER
set ::tclsh(evalinput) 1} pr_optin.tcl]
variable fx_pr_consume [makeFile {set d [read stdin]
puts "PRGOT:[string trim $d]"} pr_consume.tcl]
#encoding fixture written with explicit utf-8 control (runner-encoding-proof): a single
#U+00E9 char as raw utf-8 bytes (0xC3 0xA9) - 1 char sourced via utf-8, 2 via iso8859-1.
#the char is built with [format %c 233] so this .test file stays pure ascii (immune to
#the runner's source encoding).
variable fx_enc_utf8 [file join [temporaryDirectory] enc_utf8.tcl]
apply {{path} {
set fd [open $path w]
chan configure $fd -encoding utf-8
puts $fd "puts ENCLEN=\[string length \"[format %c 233]\"\]"
close $fd
}} $fx_enc_utf8
variable common {
set result ""
}
# -- dispatch contract (any punk kit) ----------------------------------------------------------
#added 2026-07-23 (agent) - tclsh subcommand dispatch contract (script-arg sourcing, piped eval, exit codes)
test tclsh_piped_eval {piped no-arg 'tclsh' evaluates stdin and terminates, exitcode 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts TCLSHPIPE-M1\nputs TCLSHPIPE-M2\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHPIPE-M1" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "TCLSHPIPE-M2" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
test tclsh_piped_error_exitcode {piped no-arg 'tclsh' input error: error text on output, exitcode 1, no hang}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "error tclshpipe-boom\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "tclshpipe-boom" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1]
test tclsh_piped_not_interactive {piped no-arg 'tclsh' runs with tcl_interactive 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts \[list TSTATE \$::tcl_interactive\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TSTATE 0" [dict get $rd output]] >= 0}]
} -result [list 0 1]
test tclsh_script_file_argv {'tclsh <file> a b': argv0/info script = file, argv = remaining args, tcl_interactive 0 (piped launch)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_argv
set rd [punk_run $punkexe [list tclsh $fx_dispatch_argv alpha beta] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first {TARGS argv0tail dispatch_argv.tcl argv {alpha beta} scripttail dispatch_argv.tcl tcli 0} [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_script_error_exitcode {'tclsh <file>' script error: prior output present, error text on output, exitcode 1}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_err
set rd [punk_run $punkexe [list tclsh $fx_dispatch_err] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHF-BEFORE-ERR" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "tclshfile-boom" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1]
test tclsh_script_exitcode {'tclsh <file>' explicit 'exit 5' propagates exitcode 5}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_exit5
set rd [punk_run $punkexe [list tclsh $fx_dispatch_exit5] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHF-EXIT5-MARK" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 5]
# -- stock argument forms: leading dash, -encoding, lib: refusal -------------------------------
#added 2026-07-23 (agent) - stock leading-dash/-encoding argument forms + lib: pointer refusal (tclsh subcommand)
test tclsh_dash_args_to_argv {leading '-' arg is not a script: all args stay in ::argv, stdin evaluated (stock parity)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh - hmm etc blah] "puts \[list DASHARGS \$::argv\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "DASHARGS {- hmm etc blah}" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_encoding_applied {'-encoding name file' form: the named encoding is honoured when sourcing}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_enc_utf8
set a [punk_run $punkexe [list tclsh -encoding utf-8 $fx_enc_utf8] ""]
set b [punk_run $punkexe [list tclsh -encoding iso8859-1 $fx_enc_utf8] ""]
lappend result [expr {[string first "ENCLEN=1" [dict get $a output]] >= 0}]
lappend result [expr {[string first "ENCLEN=2" [dict get $b output]] >= 0}]
lappend result [dict get $a exitcode]
lappend result [dict get $b exitcode]
} -result [list 1 1 0 0]
test tclsh_encoding_args_passed {'-encoding name file a b': script argv0/argv state as with the plain file form}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_argv
set rd [punk_run $punkexe [list tclsh -encoding utf-8 $fx_dispatch_argv alpha beta] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first {TARGS argv0tail dispatch_argv.tcl argv {alpha beta} scripttail dispatch_argv.tcl tcli 0} [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_encoding_incomplete_form {'-encoding name' with no script name following: all args stay in ::argv (stock parity)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh -encoding utf-8] "puts \[list ENCARGV \$::argv\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "ENCARGV {-encoding utf-8}" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_lib_pointer {'tclsh lib:...' refuses with a pointer to the script subcommand, exit 1, nothing sourced}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh lib:_punktest/echo alpha] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "not supported by the tclsh subcommand" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "script lib:_punktest/echo" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "PUNKTEST_ECHO" [dict get $rd output]] < 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1 1]
# -- piperepl launch-state contract (patched runtime kits) -------------------------------------
#added 2026-07-23 (agent) - piperepl ::tclsh launch-state contract via the tclsh subcommand (G-096/G-103 patched kits)
test tclsh_pr_noarg_machinery {piped no-arg run publishes ::tclsh machinery: istty 0, dorepl 0, evalinput 0}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
set rd [punk_run $pipereplexe [list tclsh] "puts \[list PRSTATE \[info exists ::tclsh(istty)\] \$::tclsh(istty) \$::tclsh(dorepl) \$::tclsh(evalinput)\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRSTATE 1 0 0 0" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_pr_scriptarg_state {piped 'tclsh <file>' launch state: istty 0, tcl_interactive 0, dorepl 0, evalinput 0}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_state
set rd [punk_run $pipereplexe [list tclsh $fx_pr_state] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRSCRIPT istty 0 tcli 0 dorepl 0 evalinput 0" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_pr_unconsumed_not_evald {piped input a script did not consume is NOT evaluated by default (safety default)}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_noconsume
set rd [punk_run $pipereplexe [list tclsh $fx_pr_noconsume] "puts PRSHOULDNOTEVAL\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRNOCON-MARKER" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "PRSHOULDNOTEVAL" [dict get $rd output]] < 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
test tclsh_pr_evalinput_optin {script setting ::tclsh(evalinput) 1: leftover piped input evaluated after the script}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_optin
set rd [punk_run $pipereplexe [list tclsh $fx_pr_optin] "puts PREVALLED\n"]
set out [dict get $rd output]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PROPTIN-MARKER" $out] >= 0}]
lappend result [expr {[string first "PREVALLED" $out] >= 0}]
#leftover input runs AFTER the script completes
lappend result [expr {[string first "PROPTIN-MARKER" $out] < [string first "PREVALLED" $out]}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1 0]
test tclsh_pr_script_consumes_stdin {the script may read piped stdin itself; consumed data is not evaluated}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_consume
set rd [punk_run $pipereplexe [list tclsh $fx_pr_consume] "pr-hello-data\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRGOT:pr-hello-data" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
# -- degraded mode (kit runtime without the piperepl patch) ------------------------------------
#added 2026-07-23 (agent) - degraded-mode contract: unpatched runtime still evaluates piped input, with a patch-missing notice
test tclsh_unpatched_notice_and_eval {unpatched runtime: piped no-arg eval still works, with a piperepl notice on stderr}\
-constraints pipereplmissing -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts TCLSHUNP-MARK\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHUNP-MARK" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "piperepl" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
}
tcltest::cleanupTests ;#needed to produce test summary.

63
src/vfs/_config/punk_main.tcl

@ -1216,17 +1216,17 @@ apply { args {
#we would like to drop through to standard tclsh repl without launching another process #we would like to drop through to standard tclsh repl without launching another process
#tclMain.c doesn't allow it unless patched. #tclMain.c doesn't allow it unless patched.
if {![info exists ::env(TCLSH_PIPEREPL)]} { if {![info exists ::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true 0 set is_tclsh_piperepl_env_true 1
} else { } else {
if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} { if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL) set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL)
} else { } else {
set is_tclsh_piperepl_env_true 0 set is_tclsh_piperepl_env_true 1
} }
} }
if {!$is_tclsh_piperepl_env_true} { if {!$is_tclsh_piperepl_env_true} {
puts stderr "tcl_interactive: $::tcl_interactive" puts stderr "tcl_interactive: $::tcl_interactive"
puts stderr "stdin: [chan configure stdin]" #puts stderr "stdin: [chan configure stdin]"
puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean" puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean"
} else { } else {
#according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired #according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired
@ -1235,16 +1235,61 @@ apply { args {
puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch" puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch"
} }
} }
set ::tcl_interactive 1 #stock tclsh argument forms (tclMain.c): the only recognised leading option is
set ::tclsh(dorepl) 1 #'-encoding name fileName' (and only when fileName does not begin with '-');
if {[llength $subcommand_arglist]} { #any other leading '-' argument means NO script file - all arguments stay in
set normscript [file normalize [lindex $subcommand_arglist 0]] #::argv (already set above) and tclsh proceeds to the repl (tty) or stdin
#evaluation (piped).
set tclsh_have_script 0
set tclsh_encoding ""
if {[llength $subcommand_arglist] >= 3 && [lindex $subcommand_arglist 0] eq "-encoding" && ![string match -* [lindex $subcommand_arglist 2]]} {
set tclsh_encoding [lindex $subcommand_arglist 1]
set tclsh_script [lindex $subcommand_arglist 2]
set tclsh_scriptargs [lrange $subcommand_arglist 3 end]
set tclsh_have_script 1
} elseif {[llength $subcommand_arglist] && ![string match -* [lindex $subcommand_arglist 0]]} {
set tclsh_script [lindex $subcommand_arglist 0]
set tclsh_scriptargs [lrange $subcommand_arglist 1 end]
set tclsh_have_script 1
}
if {$tclsh_have_script} {
if {[string match -nocase lib:* $tclsh_script]} {
#scriptlib resolution is a punk facility - the tclsh subcommand keeps plain
#tclsh semantics (no punk modules loaded), so point at the 'script' subcommand
#instead of failing on a literal 'lib:...' path (illegal on windows filesystems
#anyway; reachable via ./lib:... or an absolute path on other platforms).
set exebase [file rootname [file tail [info nameofexecutable]]]
puts stderr "punk tclsh: 'lib:' scriptlib resolution is not supported by the tclsh subcommand (plain tclsh semantics)"
puts stderr " use: $exebase script $tclsh_script ?args...?"
exit 1
}
set normscript [file normalize $tclsh_script]
info script $normscript info script $normscript
set ::argv0 $normscript set ::argv0 $normscript
set ::argv [lrange $subcommand_arglist 1 end] set ::argv $tclsh_scriptargs
set ::argc [llength $::argv] set ::argc [llength $::argv]
#we are in an apply context here - so we need to uplevel to get the source to work as expected #we are in an apply context here - so we need to uplevel to get the source to work as expected
uplevel 1 [list source [lindex $subcommand_arglist 0]] if {$tclsh_encoding ne ""} {
uplevel 1 [list source -encoding $tclsh_encoding $tclsh_script]
} else {
uplevel 1 [list source $tclsh_script]
}
#default tclsh behaviour is to run the script and exit
#the script can set ::tclsh(dorepl) 1 to force the tclsh repl after the script has run
} else {
if {[info exists ::tclsh(istty)] && $::tclsh(istty)} {
#tclsh piperepl patch applied - stdin is a tty - we can run the tclsh repl
set ::tclsh(dorepl) 1
set ::tcl_interactive 1
} else {
#stdin is not a tty - we are being run with input piped in - we will evaluate stdin as a script and exit
set ::tclsh(dorepl) 0
set ::tcl_interactive 0
#script on stdin could set ::tclsh(dorepl) 1 to force the tclsh repl after the script has run
set data [read stdin]
uplevel 1 [list eval $data]
}
} }
} }
shellspy { shellspy {

Loading…
Cancel
Save