Browse Source

punk-runtime list/use stop offering support files as runtimes (punkshell 0.27.3)

'bin/punk-runtime.cmd list -platform win32-ix86' listed a LUCK.url shortcut and a
tclsh8.6.10-luck-zip.cfg build request as if they were selectable runtimes. Both payloads
already filtered support files - .txt/.toml/.tm/.tmp/.log - so the mechanism existed and
only the recognised set was too narrow.

It was also duplicated across NINE filter sites in three syntaxes (a powershell array, a
bash case pattern, a bash grep -E regex), and two of them had already drifted: the "not a
selectable runtime" guard omitted .log and matched *_BUILDCOPY without a trailing wildcard.
The set is now defined ONCE per payload - $PunkRuntimeSupportExt /
Test-PunkRuntimeSupportName in the ps1, punk_support_ext_re / is_punk_support_name in the
bash - and every site consults it, so the two payloads cannot drift apart. Their listings
are required to agree byte-for-byte and now do so by construction.

Recognised support files, by category: docs/text (.txt .md .html .htm .pdf .url),
config/metadata (.toml .json .xml .yml .yaml .ini .cfg), dev leftovers
(.tm .tmp .log .bak .old), checksums/signatures (.sha1 .sha256 .asc .sig) and archives
(.zip .tar .gz .bz2 .xz .7z), plus any *_BUILDCOPY* name. Anything else is still treated as
a runtime candidate - deliberately, so a stray binary is visible rather than silently
hidden.

Verified: the win32-ix86 listing drops from 6 entries to the 4 real runtimes; the bash
helpers produce the identical four (the polyglot routes msys bash to the ps1 payload on
windows, so the bash side was exercised directly). Re-wrapped per bin/AGENTS.md with
checkfile ERROR-free; runtimecmd_roundtrip (byte-identity), runtimecmd_checkfile,
runtimecmd_freshness (10/10, both payloads against the fixture server) and runtimebash_wsl
(8/8) all pass, as does the full suite at its documented baseline.

bin/runtime/README.md now states what a platform folder is FOR (a store tier keyed by the
platform a runtime is for, not by this machine) and what is tolerated in it, with the
recognised-support-file table and the warning that anything else will be offered as a
runtime. It is force-added to git: /bin/* is ignored and a ! negation cannot re-include a
file inside an excluded directory, so git add -f is the only mechanism - recorded in the
.fossil-settings/AGENTS.md force-tracked one-off list, whose fossil counterpart is an
explicit 'fossil add' at the next catch-up checkin.

Also records in G-101 a survey of TEMP_REFERENCE/androwish for the zipfs-on-8.6 question
the developer raised (non-contract findings; pointer added to TEMP_REFERENCE/AGENTS.md per
its contract so the survey is not repeated). The undroidwish zipfs backport is ADDITIVE -
generic/zipfs.c (6586 lines) + zipfs.h + a library/zipfs1.0 dir + one object line in the
win and unix Makefiles - so it is carryable as a suite patch rather than a fork; it is C
rather than C++ like mk4tcl; Tcl_StaticPackage registration is gated on ZIPFS_IN_TCL /
ZIPFS_IN_TK, i.e. designed to compile into tcl or tk; and it is Christian Werner's, under
the tree's Tcl-style license.terms. Its capability delta against Tcl 9 zipfs is recorded
(no 'root', no ::zipfs ensemble, and not registered in child or thread interps), as is the
exe-path mount convention that motivates G-129. Provenance of our specimen is established:
LUCK is a CGI repacker that selects packages out of a prebuilt vanillatclsh binary's ZIP,
so bin/runtime/win32-ix86/tclsh8.6.10-luck-zip.exe is a repack of androwish's own build -
a working existence proof of zipfs-on-8.6 rather than a claim. Three open questions are
recorded rather than guessed: whether it applies to 8.6.18 (the tree is 8.6.10), whether it
builds under the pinned zig, and where the boot-time self-mount hook lives - stock
tclAppInit calls Tclzipfs_Init only under #ifdef TCL_TEST, so it is in the
vanillatclsh/undroidwish application main, which is also the hook a dual-container
autodetecting boot would need.

Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
176f2ad702
  1. 2
      .fossil-settings/AGENTS.md
  2. 18
      CHANGELOG.md
  3. 11
      bin/AGENTS.md
  4. 79
      bin/punk-runtime.cmd
  5. 61
      bin/runtime/README.md
  6. 50
      goals/G-101-tcl86-kit-container-strategy.md
  7. 2
      punkproject.toml
  8. 45
      src/scriptapps/bin/punk-runtime.bash
  9. 34
      src/scriptapps/bin/punk-runtime.ps1

2
.fossil-settings/AGENTS.md

@ -25,7 +25,7 @@ Versioned fossil settings (one setting value or glob list per file) for this dua
### ignore-glob derivation rules (verified against fossil 2.28) ### ignore-glob derivation rules (verified against fossil 2.28)
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, `.claude/settings.json` (shared claude harness hooks/permissions; session-local `.claude` files stay ignored), `.claude/skills/**` (project agent skills - synced copies of `.agents/skills/**`, which is not git-ignored and needs no exception), plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list. - **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, `.claude/settings.json` (shared claude harness hooks/permissions; session-local `.claude` files stay ignored), `.claude/skills/**` (project agent skills - synced copies of `.agents/skills/**`, which is not git-ignored and needs no exception), plus force-tracked one-offs (`bin/libssp-0.dll`, `bin/runtime/README.md` - guidance, not build output; a `!` negation cannot re-include it because git does not descend into the excluded `bin/runtime` directory, so `git add -f` is the only mechanism, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list.
- A bare directory name prunes that whole tree; `*` crosses `/`; git patterns intended to match at any depth need an additional `*/` variant; `#` comment lines are honoured. - A bare directory name prunes that whole tree; `*` crosses `/`; git patterns intended to match at any depth need an additional `*/` variant; `#` comment lines are honoured.
- Do not add the fossil-generated names (`manifest`, `manifest.uuid`, `manifest.tags`) to `ignore-glob` - fossil handles them itself. - Do not add the fossil-generated names (`manifest`, `manifest.uuid`, `manifest.tags`) to `ignore-glob` - fossil handles them itself.
- **Nested `.gitignore` files are git-only**: ignore files inside subtrees (e.g. the project-layout templates under `src/project_layouts/`) are honoured by git as nested ignores of the outer repo but invisible to fossil - template content they match is untracked in git yet managed by fossil. Treat such divergence as a signal to review the git side (usually `git add -f` of the affected template files). - **Nested `.gitignore` files are git-only**: ignore files inside subtrees (e.g. the project-layout templates under `src/project_layouts/`) are honoured by git as nested ignores of the outer repo but invisible to fossil - template content they match is untracked in git yet managed by fossil. Treat such divergence as a signal to review the git side (usually `git add -f` of the affected template files).

18
CHANGELOG.md

@ -5,6 +5,24 @@ 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.27.3] - 2026-07-27
- `bin/punk-runtime.cmd list` and `use` no longer offer support files as selectable
runtimes. The payloads already filtered `.txt/.toml/.tm/.tmp/.log`; the recognised set
now also covers docs/text (`.md .html .htm .pdf .url`), config/metadata
(`.json .xml .yml .yaml .ini .cfg`), dev leftovers (`.bak .old`), checksums and
signatures (`.sha1 .sha256 .asc .sig`) and archives (`.zip .tar .gz .bz2 .xz .7z`).
Prompted by a runtime tier where a build-request `.cfg` and a `.url` shortcut were
listed as runtimes.
- The set is now defined ONCE per payload (`$PunkRuntimeSupportExt` /
`Test-PunkRuntimeSupportName` in the powershell payload, `punk_support_ext_re` /
`is_punk_support_name` in the bash payload) instead of repeated at each of the nine
filter sites, so the two payloads cannot drift - their listings are required to agree
byte-for-byte.
- `bin/runtime/README.md` now states what a platform folder is for and what is tolerated
in it: anything not recognised as a support file is offered as a runtime, so unrelated
executables do not belong there.
## [0.27.2] - 2026-07-27 ## [0.27.2] - 2026-07-27
- The bake's boot-precondition gate (G-125) recognises a third tcl-library convention: - The bake's boot-precondition gate (G-125) recognises a third tcl-library convention:

11
bin/AGENTS.md

@ -143,7 +143,16 @@ suffix (2026-07-25; exact names - incl. extensionless unix artifacts in foreign
`-platform` folders - match first, then the `.exe`-suffixed spelling). Root-name `-platform` folders - match first, then the `.exe`-suffixed spelling). Root-name
handling strips only a `.exe` suffix handling strips only a `.exe` suffix
(dotted tcl patchlevels make generic last-dot stripping wrong for extensionless unix (dotted tcl patchlevels make generic last-dot stripping wrong for extensionless unix
names). Candidate listing excludes directories and `.txt/.toml/.tm/.tmp/.log` files. names). Candidate listing excludes directories, `*_BUILDCOPY*` names, and SUPPORT FILES
recognised by extension - docs/text (`.txt .md .html .htm .pdf .url`), config/metadata
(`.toml .json .xml .yml .yaml .ini .cfg`), dev leftovers (`.tm .tmp .log .bak .old`),
checksums/signatures (`.sha1 .sha256 .asc .sig`) and archives
(`.zip .tar .gz .bz2 .xz .7z`). The same set filters `use` and the sha1 value-matching
against the server's sha1sums (support rows ride in that file too). It is defined ONCE per
payload - `$PunkRuntimeSupportExt`/`Test-PunkRuntimeSupportName` in the ps1,
`punk_support_ext_re`/`is_punk_support_name` in the bash - and the two must stay in step
because the listings are required to agree byte-for-byte; extend both and re-wrap. What a
tier is expected to contain is documented for users in `bin/runtime/README.md`.
Forgot-to-switch-back guard (2026-07-25): `make.tcl bake`/`bakehouse` emit a Forgot-to-switch-back guard (2026-07-25): `make.tcl bake`/`bakehouse` emit a
BUILD-WARNING (recapped at end of run) when a working runtime they wrap is materialized BUILD-WARNING (recapped at end of run) when a working runtime they wrap is materialized
from an older revision than an `-r<N>` artifact present beside it - a deliberate from an older revision than an `-r<N>` artifact present beside it - a deliberate

79
bin/punk-runtime.cmd

@ -1453,6 +1453,30 @@ set_active() {
echo "active runtime for $archtail set to: $1 (recorded in $active_file)" echo "active runtime for $archtail set to: $1 (recorded in $active_file)"
} }
#installed runtime candidates - exclude directories, build copies and #installed runtime candidates - exclude directories, build copies and
#Support-file extensions: things that legitimately sit in a runtime tier beside the
#runtimes but are never themselves selectable runtimes. Filtered from the local
#candidate listing, from 'use', and from sha1 value-matching against the server's
#sha1sums (support rows ride in that file too). Keep this in step with the ps1
#payload's $PunkRuntimeSupportExt/Test-PunkRuntimeSupportName - listings must agree
#byte-for-byte.
# docs/text .txt .md .html .htm .pdf .url
# config/meta .toml .json .xml .yml .yaml .ini .cfg
# dev leftover .tm .tmp .log .bak .old
# checksums .sha1 .sha256 .asc .sig
# archives .zip .tar .gz .bz2 .xz .7z
#Anything else is treated as a runtime candidate - see bin/runtime/README.md for what
#a runtime tier is expected to contain.
punk_support_ext_re='\.(txt|md|html|htm|pdf|url|toml|json|xml|yml|yaml|ini|cfg|tm|tmp|log|bak|old|sha1|sha256|asc|sig|zip|tar|gz|bz2|xz|7z)$'
is_punk_support_name() {
case "$1" in
*.txt|*.md|*.html|*.htm|*.pdf|*.url) return 0;;
*.toml|*.json|*.xml|*.yml|*.yaml|*.ini|*.cfg) return 0;;
*.tm|*.tmp|*.log|*.bak|*.old) return 0;;
*.sha1|*.sha256|*.asc|*.sig) return 0;;
*.zip|*.tar|*.gz|*.bz2|*.xz|*.7z) return 0;;
esac
return 1
}
#non-runtime files (ps1-payload parity: files only, same extension excludes). #non-runtime files (ps1-payload parity: files only, same extension excludes).
#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order #LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order
#matches the ps1 payload's ordinal sorting, so listings agree everywhere. #matches the ps1 payload's ordinal sorting, so listings agree everywhere.
@ -1463,8 +1487,9 @@ list_candidates() {
[[ -f "$f" ]] || continue [[ -f "$f" ]] || continue
f="${f##*/}" f="${f##*/}"
case "$f" in case "$f" in
*_BUILDCOPY*|*.txt|*.toml|*.tm|*.tmp|*.log) continue;; *_BUILDCOPY*) continue;;
esac esac
is_punk_support_name "$f" && continue
printf '%s\n' "$f" printf '%s\n' "$f"
done done
fi fi
@ -1691,7 +1716,7 @@ freshness_verdict() {
fi fi
#deterministic pick: C-collation first value-match, support files #deterministic pick: C-collation first value-match, support files
#(.txt/.toml/...) filtered before the pick (ps1-payload parity) #(.txt/.toml/...) filtered before the pick (ps1-payload parity)
vmatch=$(tr -d '\r' < "$sha1file" | awk -v h="$asha" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev '\.(txt|toml|tm|tmp|log)$' | LC_ALL=C sort | head -n 1) vmatch=$(tr -d '\r' < "$sha1file" | awk -v h="$asha" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev "$punk_support_ext_re" | LC_ALL=C sort | head -n 1)
fi fi
if [[ -n "$vmatch" ]]; then if [[ -n "$vmatch" ]]; then
mrev=$(rev_of "$vmatch") mrev=$(rev_of "$vmatch")
@ -1958,7 +1983,7 @@ case "$action" in
[[ -n "$dsha" && "$local_sha1" == "$dsha" ]] && vname="$platform_default" [[ -n "$dsha" && "$local_sha1" == "$dsha" ]] && vname="$platform_default"
fi fi
if [[ -z "$vname" ]]; then if [[ -z "$vname" ]]; then
vname=$(tr -d '\r' < "$sha1local" | awk -v h="$local_sha1" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev '\.(txt|toml|tm|tmp|log)$' | LC_ALL=C sort | head -n 1) vname=$(tr -d '\r' < "$sha1local" | awk -v h="$local_sha1" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev "$punk_support_ext_re" | LC_ALL=C sort | head -n 1)
fi fi
if [[ -n "$vname" ]]; then if [[ -n "$vname" ]]; then
mid="sha1=$vname" mid="sha1=$vname"
@ -1987,9 +2012,7 @@ case "$action" in
#skip support files riding in the server's sha1sums beside the #skip support files riding in the server's sha1sums beside the
#runtimes (artifact metadata tomls etc) - same extension set the #runtimes (artifact metadata tomls etc) - same extension set the
#local candidate filter excludes #local candidate filter excludes
case "$rname" in is_punk_support_name "$rname" && continue
*.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)"
@ -2040,12 +2063,10 @@ case "$action" in
fi fi
exit 1 exit 1
fi fi
case "$1" in if [[ "$1" == *_BUILDCOPY* ]] || is_punk_support_name "$1"; then
*_BUILDCOPY|*.txt|*.toml|*.tm|*.tmp) echo "'$1' is not a selectable runtime"
echo "'$1' is not a selectable runtime" exit 1
exit 1 fi
;;
esac
if [[ ! -f "$archdir/$1" && -f "$archdir/$1.exe" ]]; then if [[ ! -f "$archdir/$1" && -f "$archdir/$1.exe" ]]; then
#accept the name without its .exe suffix (root-name convention: G-103 #accept the name without its .exe suffix (root-name convention: G-103
#name handling strips only .exe, so the suffixless spelling is #name handling strips only .exe, so the suffixless spelling is
@ -2671,6 +2692,29 @@ function Set-PunkActiveRuntime {
[System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false))
Write-Host "active runtime set to: $name (recorded in $activefile)" Write-Host "active runtime set to: $name (recorded in $activefile)"
} }
#Support-file extensions: things that legitimately sit in a runtime tier beside the
#runtimes but are never themselves selectable runtimes. Filtered from the local
#candidate listing, from 'use', and from sha1 value-matching against the server's
#sha1sums (support rows ride in that file too). Keep this set and the bash payload's
#is_punk_support_name/punk_support_ext_re in step - listings must agree byte-for-byte.
# docs/text .txt .md .html .htm .pdf .url
# config/meta .toml .json .xml .yml .yaml .ini .cfg
# dev leftover .tm .tmp .log .bak .old
# checksums .sha1 .sha256 .asc .sig
# archives .zip .tar .gz .bz2 .xz .7z
#Anything else is treated as a runtime candidate - see bin/runtime/README.md for what
#a runtime tier is expected to contain.
$script:PunkRuntimeSupportExt = @(
".txt",".md",".html",".htm",".pdf",".url",
".toml",".json",".xml",".yml",".yaml",".ini",".cfg",
".tm",".tmp",".log",".bak",".old",
".sha1",".sha256",".asc",".sig",
".zip",".tar",".gz",".bz2",".xz",".7z"
)
function Test-PunkRuntimeSupportName {
param([string] $name)
return ($script:PunkRuntimeSupportExt -contains [System.IO.Path]::GetExtension($name).ToLowerInvariant())
}
#installed runtime candidates - exclude build copies and non-runtime files. #installed runtime candidates - exclude build copies and non-runtime files.
#ORDINAL name order: culture-sensitive Sort-Object collates differently between #ORDINAL name order: culture-sensitive Sort-Object collates differently between
#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 #windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1
@ -2681,7 +2725,7 @@ function Get-PunkRuntimeCandidates {
if (-not (Test-Path -Path $archfolder -PathType Container)) { if (-not (Test-Path -Path $archfolder -PathType Container)) {
return @() return @()
} }
$files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not (Test-PunkRuntimeSupportName $_.Name) })
if ($files.Count -le 1) { if ($files.Count -le 1) {
return $files return $files
} }
@ -3116,8 +3160,7 @@ function Write-PunkFreshnessVerdict {
$names = @($valdict.Keys) $names = @($valdict.Keys)
[array]::Sort($names, [System.StringComparer]::Ordinal) [array]::Sort($names, [System.StringComparer]::Ordinal)
foreach ($n in $names) { foreach ($n in $names) {
$rext = [System.IO.Path]::GetExtension($n) if (Test-PunkRuntimeSupportName $n) { continue }
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
if ($valdict[$n] -eq $asha) { $vmatch = $n; break } if ($valdict[$n] -eq $asha) { $vmatch = $n; break }
} }
} }
@ -3785,8 +3828,7 @@ function psmain {
$rnames = @($remotedict.Keys) $rnames = @($remotedict.Keys)
[array]::Sort($rnames, [System.StringComparer]::Ordinal) [array]::Sort($rnames, [System.StringComparer]::Ordinal)
foreach ($rn in $rnames) { foreach ($rn in $rnames) {
$rext = [System.IO.Path]::GetExtension($rn) if (Test-PunkRuntimeSupportName $rn) { continue }
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
if ($remotedict[$rn] -eq $local_sha1) { $vname = $rn; break } if ($remotedict[$rn] -eq $local_sha1) { $vname = $rn; break }
} }
} }
@ -3821,8 +3863,7 @@ function psmain {
#skip support files riding in the server's sha1sums beside #skip support files riding in the server's sha1sums beside
#the runtimes (artifact metadata tomls etc) - same extension #the runtimes (artifact metadata tomls etc) - same extension
#set the local candidate filter excludes #set the local candidate filter excludes
$rext = [System.IO.Path]::GetExtension($key) if (Test-PunkRuntimeSupportName $key) { continue }
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 (" {0,-35} {1,-33} " -f "-", "") write-host -nonewline (" {0,-35} {1,-33} " -f "-", "")

61
bin/runtime/README.md

@ -0,0 +1,61 @@
runtime Tcl Executables are installed here under the appropriate os_architecture folder.
e.g
windows 64bit binaries under win32-x86_64
windows 32bit binaries under win32-ix86
linux 64 bit binaries under linux-x86_64
## What belongs in a platform folder
A platform folder is a store TIER: it holds runtime executables FOR that platform. The
folder need not match the machine you are on - a tier for another platform is a
first-class thing to populate, because the kit build reads each kit's tier from that
kit's target platform rather than from the driving tclsh (see src/AGENTS.md).
Everything in the folder that is not recognised as a support file is treated as a
selectable runtime by `bin/punk-runtime.cmd list` and `use`. Support files are
recognised by extension and are tolerated beside the runtimes without appearing as
candidates:
docs/text .txt .md .html .htm .pdf .url
config/meta .toml .json .xml .yml .yaml .ini .cfg
dev leftovers .tm .tmp .log .bak .old
checksums .sha1 .sha256 .asc .sig
archives .zip .tar .gz .bz2 .xz .7z
plus any name containing `_BUILDCOPY`. That covers what legitimately accumulates here:
`active.toml` (which runtime `run` launches - per-machine, not versioned), a runtime's
`<rootname>.toml` artifact-metadata record, a cached `sha1sums.txt`, a downloaded
checksum or signature, notes about where a runtime came from, and a build-request file
such as the `.cfg` a Lean Undroidwish Construction Kit build is reproducible from.
Anything else - including a stray binary that is not a Tcl runtime - WILL be offered as
a runtime, so keep unrelated executables out of these folders. If a new kind of support
file starts appearing here regularly, extend the recognised set in BOTH payloads
(`src/scriptapps/bin/punk-runtime.ps1` and `.bash` - see the shared
`PunkRuntimeSupportExt` / `is_punk_support_name` definitions) and re-wrap; the two
listings are required to agree byte-for-byte.
The folders themselves are not versioned (the `bin/*` ignore globs cover them), so
nothing here is a place to keep anything you cannot re-fetch or rebuild.
The bin/punk-runtime.cmd cross-platform script can be used to download prebuilt Tcl binaries.
e.g ./bin/punk-runtime.cmd fetch
This will detect the platform and architecture, creating the containing folder as necessary.
To see existing installed runtimes:
./bin/punk-runtime.cmd list
Or to compare local runtimes with those available on the artifact server:
./bin/punk-runtime.cmd list -remote
An installed runtime can then be used to call the src/make.tcl file.
e.g - initial check
./bin/punk-runtime.cmd run src/make.tcl check
e.g - to build (or just copy with latest version number from src if build not required) modules to projectdir/modules
./bin/punk-runtime.cmd run src/make.tcl modules
for libs & modules
./bin/punk-runtime.cmd run src/make.tcl packages

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

@ -291,6 +291,56 @@ shape via its /app anchor, so this is not an mk4-specific wart):
a runtime wrapping a DIFFERENT copy of itself, so the shadowing path is a runtime wrapping a DIFFERENT copy of itself, so the shadowing path is
exercised rather than accidentally avoided. exercised rather than accidentally avoided.
## zipfs backported to 8.6 (androwish/undroidwish - bearing on container choice)
Recorded 2026-07-27 from a survey of `TEMP_REFERENCE/androwish` (fossil checkout;
the tree is Tcl 8.6.10). The developer raised it as a third possibility beside mk4
and cookfs: rather than choosing a non-zipfs container for 8.6, bring zipfs itself
to the suite-built 8.6 runtime, as undroidwish does.
- SHAPE IS ADDITIVE, which is what makes it tractable. `jni/tcl/generic/zipfs.c`
(6586 lines) plus `jni/tcl/generic/zipfs.h` (70), a `jni/tcl/library/zipfs1.0/`
package dir, and one object added in `win/Makefile.in:314` and
`unix/Makefile.in:309`. It is a NEW FILE plus build wiring, not a diffuse patch
spread through the core - so it can be carried as a suite patch of the kind
`src/buildsuites/suite_tcl86/patches/` already holds, rather than a fork.
- It does use core internals (`#include "tclInt.h"`, `"tclFileSystem.h"`), so it is
pinned to the core version it is compiled against. UNVERIFIED and the first thing
to check: whether it applies to 8.6.18 (what suite_tcl86 builds) - the reference
tree is 8.6.10.
- Entry point `Zipfs_Init`, aliased `Tclzipfs_Init` / `Tkzipfs_Init` (zipfs.h:30,42).
The `Tcl_StaticPackage(interp, "zipfs", ...)` registration is gated on
`ZIPFS_IN_TCL` / `ZIPFS_IN_TK` (zipfs.c:6445-6447) - it is designed to be compiled
INTO tcl or tk, which is exactly the shape this goal would want.
- LICENCE: Copyright (c) 2013-2023 Christian Werner, under the tree's
`license.terms` (Tcl-style BSD) - compatible with punkshell's BSD-2-Clause, and a
single named upstream author to attribute per G-063.
- CAPABILITY DELTA against Tcl 9's zipfs, measured on the shipped runtime (see the
table in goals/G-129-kitboot-mountpoint-derivation.md): it provides
`mount unmount info list exists mkzip mkimg mkkey lmkzip lmkimg` but NOT `root`,
provides no `::zipfs` ensemble, and is NOT registered in child or thread interps
(16 commands in every Tcl 9 interp vs 10 in the 8.6 backport's MAIN interp only).
Any punkshell code that must work under it therefore cannot probe with
`tcl::zipfs::root`, cannot call a bare `zipfs`, and must not assume a codethread
can see zipfs at all.
- MOUNT CONVENTION: it mounts the executable's attached archive at the EXECUTABLE'S
OWN PATH, not at `//zipfs:/app` - the reason G-129 exists.
- WHERE THE SELF-MOUNT LIVES IS AN OPEN QUESTION. Stock `win/tclAppInit.c:192` and
`unix/tclAppInit.c:130` call `Tclzipfs_Init` only under `#ifdef TCL_TEST`, so the
boot-time self-mount is in the vanillatclsh/undroidwish application main rather
than in the stock appinit. Locating that hook is prerequisite work for adopting
the backport, and it is the same hook a dual-container autodetecting boot would
need.
- PROVENANCE OF OUR SPECIMEN: LUCK (`undroid/luck/`) is a CGI script that repacks a
PREBUILT `vanillatclsh-*` / `undroidwish-*` binary, selecting packages out of that
binary's ZIP using Memchan channels and in-memory ZIP mounts (no temp files). So
`bin/runtime/win32-ix86/tclsh8.6.10-luck-zip.exe` is a repack of androwish's own
`vanillatclsh` build, and the base binaries come from the tree's
`undroid/build-undroidwish-*.sh` scripts. That makes the runtime a working
existence proof of zipfs-on-8.6, not merely a claim.
- UNVERIFIED: whether zipfs.c builds under the pinned zig toolchain. It is C (not
C++ like mk4tcl), which is a point in its favour against the metakit candidate.
## Metakit under Tcl 9 (bearing on container choice) ## Metakit under Tcl 9 (bearing on container choice)
Recorded 2026-07-26 from an API survey against TEMP_REFERENCE/tcl9 (9.1b1). Recorded 2026-07-26 from an API survey against TEMP_REFERENCE/tcl9 (9.1b1).

2
punkproject.toml

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

45
src/scriptapps/bin/punk-runtime.bash

@ -173,6 +173,30 @@ set_active() {
echo "active runtime for $archtail set to: $1 (recorded in $active_file)" echo "active runtime for $archtail set to: $1 (recorded in $active_file)"
} }
#installed runtime candidates - exclude directories, build copies and #installed runtime candidates - exclude directories, build copies and
#Support-file extensions: things that legitimately sit in a runtime tier beside the
#runtimes but are never themselves selectable runtimes. Filtered from the local
#candidate listing, from 'use', and from sha1 value-matching against the server's
#sha1sums (support rows ride in that file too). Keep this in step with the ps1
#payload's $PunkRuntimeSupportExt/Test-PunkRuntimeSupportName - listings must agree
#byte-for-byte.
# docs/text .txt .md .html .htm .pdf .url
# config/meta .toml .json .xml .yml .yaml .ini .cfg
# dev leftover .tm .tmp .log .bak .old
# checksums .sha1 .sha256 .asc .sig
# archives .zip .tar .gz .bz2 .xz .7z
#Anything else is treated as a runtime candidate - see bin/runtime/README.md for what
#a runtime tier is expected to contain.
punk_support_ext_re='\.(txt|md|html|htm|pdf|url|toml|json|xml|yml|yaml|ini|cfg|tm|tmp|log|bak|old|sha1|sha256|asc|sig|zip|tar|gz|bz2|xz|7z)$'
is_punk_support_name() {
case "$1" in
*.txt|*.md|*.html|*.htm|*.pdf|*.url) return 0;;
*.toml|*.json|*.xml|*.yml|*.yaml|*.ini|*.cfg) return 0;;
*.tm|*.tmp|*.log|*.bak|*.old) return 0;;
*.sha1|*.sha256|*.asc|*.sig) return 0;;
*.zip|*.tar|*.gz|*.bz2|*.xz|*.7z) return 0;;
esac
return 1
}
#non-runtime files (ps1-payload parity: files only, same extension excludes). #non-runtime files (ps1-payload parity: files only, same extension excludes).
#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order #LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order
#matches the ps1 payload's ordinal sorting, so listings agree everywhere. #matches the ps1 payload's ordinal sorting, so listings agree everywhere.
@ -183,8 +207,9 @@ list_candidates() {
[[ -f "$f" ]] || continue [[ -f "$f" ]] || continue
f="${f##*/}" f="${f##*/}"
case "$f" in case "$f" in
*_BUILDCOPY*|*.txt|*.toml|*.tm|*.tmp|*.log) continue;; *_BUILDCOPY*) continue;;
esac esac
is_punk_support_name "$f" && continue
printf '%s\n' "$f" printf '%s\n' "$f"
done done
fi fi
@ -411,7 +436,7 @@ freshness_verdict() {
fi fi
#deterministic pick: C-collation first value-match, support files #deterministic pick: C-collation first value-match, support files
#(.txt/.toml/...) filtered before the pick (ps1-payload parity) #(.txt/.toml/...) filtered before the pick (ps1-payload parity)
vmatch=$(tr -d '\r' < "$sha1file" | awk -v h="$asha" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev '\.(txt|toml|tm|tmp|log)$' | LC_ALL=C sort | head -n 1) vmatch=$(tr -d '\r' < "$sha1file" | awk -v h="$asha" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev "$punk_support_ext_re" | LC_ALL=C sort | head -n 1)
fi fi
if [[ -n "$vmatch" ]]; then if [[ -n "$vmatch" ]]; then
mrev=$(rev_of "$vmatch") mrev=$(rev_of "$vmatch")
@ -678,7 +703,7 @@ case "$action" in
[[ -n "$dsha" && "$local_sha1" == "$dsha" ]] && vname="$platform_default" [[ -n "$dsha" && "$local_sha1" == "$dsha" ]] && vname="$platform_default"
fi fi
if [[ -z "$vname" ]]; then if [[ -z "$vname" ]]; then
vname=$(tr -d '\r' < "$sha1local" | awk -v h="$local_sha1" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev '\.(txt|toml|tm|tmp|log)$' | LC_ALL=C sort | head -n 1) vname=$(tr -d '\r' < "$sha1local" | awk -v h="$local_sha1" '{n=$2; sub(/^\*/,"",n); if (tolower($1)==h) print n}' | grep -Ev "$punk_support_ext_re" | LC_ALL=C sort | head -n 1)
fi fi
if [[ -n "$vname" ]]; then if [[ -n "$vname" ]]; then
mid="sha1=$vname" mid="sha1=$vname"
@ -707,9 +732,7 @@ case "$action" in
#skip support files riding in the server's sha1sums beside the #skip support files riding in the server's sha1sums beside the
#runtimes (artifact metadata tomls etc) - same extension set the #runtimes (artifact metadata tomls etc) - same extension set the
#local candidate filter excludes #local candidate filter excludes
case "$rname" in is_punk_support_name "$rname" && continue
*.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)"
@ -760,12 +783,10 @@ case "$action" in
fi fi
exit 1 exit 1
fi fi
case "$1" in if [[ "$1" == *_BUILDCOPY* ]] || is_punk_support_name "$1"; then
*_BUILDCOPY|*.txt|*.toml|*.tm|*.tmp) echo "'$1' is not a selectable runtime"
echo "'$1' is not a selectable runtime" exit 1
exit 1 fi
;;
esac
if [[ ! -f "$archdir/$1" && -f "$archdir/$1.exe" ]]; then if [[ ! -f "$archdir/$1" && -f "$archdir/$1.exe" ]]; then
#accept the name without its .exe suffix (root-name convention: G-103 #accept the name without its .exe suffix (root-name convention: G-103
#name handling strips only .exe, so the suffixless spelling is #name handling strips only .exe, so the suffixless spelling is

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

@ -62,6 +62,29 @@ function Set-PunkActiveRuntime {
[System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false))
Write-Host "active runtime set to: $name (recorded in $activefile)" Write-Host "active runtime set to: $name (recorded in $activefile)"
} }
#Support-file extensions: things that legitimately sit in a runtime tier beside the
#runtimes but are never themselves selectable runtimes. Filtered from the local
#candidate listing, from 'use', and from sha1 value-matching against the server's
#sha1sums (support rows ride in that file too). Keep this set and the bash payload's
#is_punk_support_name/punk_support_ext_re in step - listings must agree byte-for-byte.
# docs/text .txt .md .html .htm .pdf .url
# config/meta .toml .json .xml .yml .yaml .ini .cfg
# dev leftover .tm .tmp .log .bak .old
# checksums .sha1 .sha256 .asc .sig
# archives .zip .tar .gz .bz2 .xz .7z
#Anything else is treated as a runtime candidate - see bin/runtime/README.md for what
#a runtime tier is expected to contain.
$script:PunkRuntimeSupportExt = @(
".txt",".md",".html",".htm",".pdf",".url",
".toml",".json",".xml",".yml",".yaml",".ini",".cfg",
".tm",".tmp",".log",".bak",".old",
".sha1",".sha256",".asc",".sig",
".zip",".tar",".gz",".bz2",".xz",".7z"
)
function Test-PunkRuntimeSupportName {
param([string] $name)
return ($script:PunkRuntimeSupportExt -contains [System.IO.Path]::GetExtension($name).ToLowerInvariant())
}
#installed runtime candidates - exclude build copies and non-runtime files. #installed runtime candidates - exclude build copies and non-runtime files.
#ORDINAL name order: culture-sensitive Sort-Object collates differently between #ORDINAL name order: culture-sensitive Sort-Object collates differently between
#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 #windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1
@ -72,7 +95,7 @@ function Get-PunkRuntimeCandidates {
if (-not (Test-Path -Path $archfolder -PathType Container)) { if (-not (Test-Path -Path $archfolder -PathType Container)) {
return @() return @()
} }
$files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not (Test-PunkRuntimeSupportName $_.Name) })
if ($files.Count -le 1) { if ($files.Count -le 1) {
return $files return $files
} }
@ -507,8 +530,7 @@ function Write-PunkFreshnessVerdict {
$names = @($valdict.Keys) $names = @($valdict.Keys)
[array]::Sort($names, [System.StringComparer]::Ordinal) [array]::Sort($names, [System.StringComparer]::Ordinal)
foreach ($n in $names) { foreach ($n in $names) {
$rext = [System.IO.Path]::GetExtension($n) if (Test-PunkRuntimeSupportName $n) { continue }
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
if ($valdict[$n] -eq $asha) { $vmatch = $n; break } if ($valdict[$n] -eq $asha) { $vmatch = $n; break }
} }
} }
@ -1176,8 +1198,7 @@ function psmain {
$rnames = @($remotedict.Keys) $rnames = @($remotedict.Keys)
[array]::Sort($rnames, [System.StringComparer]::Ordinal) [array]::Sort($rnames, [System.StringComparer]::Ordinal)
foreach ($rn in $rnames) { foreach ($rn in $rnames) {
$rext = [System.IO.Path]::GetExtension($rn) if (Test-PunkRuntimeSupportName $rn) { continue }
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
if ($remotedict[$rn] -eq $local_sha1) { $vname = $rn; break } if ($remotedict[$rn] -eq $local_sha1) { $vname = $rn; break }
} }
} }
@ -1212,8 +1233,7 @@ function psmain {
#skip support files riding in the server's sha1sums beside #skip support files riding in the server's sha1sums beside
#the runtimes (artifact metadata tomls etc) - same extension #the runtimes (artifact metadata tomls etc) - same extension
#set the local candidate filter excludes #set the local candidate filter excludes
$rext = [System.IO.Path]::GetExtension($key) if (Test-PunkRuntimeSupportName $key) { continue }
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 (" {0,-35} {1,-33} " -f "-", "") write-host -nonewline (" {0,-35} {1,-33} " -f "-", "")

Loading…
Cancel
Save