From 169f064621703433d7866a2620a2e4a380f2d774 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 2 Aug 2026 21:09:41 +1000 Subject: [PATCH] G-025 landed + G-031 moduledoc: buildinfo self-report (stamp/live/source/runtime-record) + runtime-queryable boot-core API doc (0.47.0) New punk::buildinfo 0.1.0 = the single implementation (dependency-free; works in kit shells, the script env, repl code interp and tclsh-hosted shells): punkkit-stamp.toml kit stamp + live facts + governing punkproject.toml version (live_vs_stamp=same|differs) + inherited G-117 punkbin-artifact.toml record, emitted as key=value lines; absence explicit (stamp_present=0), never fabricated. make.tcl bake writes the stamp at the payload root after the G-125 gate - deterministic (NO time-of-bake field, per the G-117 embedded-record precedent; identical inputs stamp byte-identically). Boot core 0.2.0: buildinfo builtin (exit 2 on args, exit 1 unavailable, else prints report + exit 0) and publishes ::punkboot::boot_state (core_version, main_script, package_modes, proj_scope, proj_root, kit_payload_base, src_project_root) - the single boot running-state surface G-089 anticipated. Wiring: aliascore 0.2.0 buildinfo alias, repl 0.5.4 preloads punk::buildinfo (bootsupport include_modules.config gains punk::buildinfo - a repl 0.5.4 snapshot without it broke tclsh-hosted shells, caught and fixed), moduledoc::punkexe 0.3.0 documents the subcommand. New punk::args::moduledoc::punkboot 0.1.0 (G-031): thin-main contract + registration model + live defs for the persistent ::punkboot procs - all six ids resolve in-kit via punk::args::get_ids after update_definitions. Verified: module fixtures 8/8 (report.test); punkexe suite 118 total 114 pass 0 fail incl new buildinfo.test (stdout-only + exit 0, stamp fields, RENAMED-COPY reports its stamp, args rejected exit 2); punk902z + punk9_beta bakes stamped (punk9_beta cites runtime build_id 635e7951... + r2 artifact name from the inherited record); tclsh-hosted 'make.tcl shell' reports stamp_present=0 with real host exe + live project version; double-bake stamp sha1-identical. Layout _config/_vfscommon core copies refreshed 0.2.0. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 25 ++ punkproject.toml | 2 +- .../modules/include_modules.config | 1 + src/make.tcl | 67 ++++ src/modules/punk/aliascore-999999.0a1.0.tm | 1 + src/modules/punk/aliascore-buildversion.txt | 3 +- .../args/moduledoc/punkboot-999999.0a1.0.tm | 221 ++++++++++++ .../args/moduledoc/punkboot-buildversion.txt | 4 + .../args/moduledoc/punkexe-999999.0a1.0.tm | 41 ++- .../args/moduledoc/punkexe-buildversion.txt | 3 +- src/modules/punk/buildinfo-999999.0a1.0.tm | 334 ++++++++++++++++++ src/modules/punk/buildinfo-buildversion.txt | 4 + src/modules/punk/repl-999999.0a1.0.tm | 1 + src/modules/punk/repl-buildversion.txt | 3 +- .../src/vfs/_config/punkboot_core.tcl | 51 ++- .../src/vfs/_vfscommon.vfs/punkboot/core.tcl | 51 ++- .../testsuites/buildinfo/report.test | 181 ++++++++++ .../shell/testsuites/punkexe/buildinfo.test | 123 +++++++ src/vfs/_config/punkboot_core.tcl | 51 ++- 19 files changed, 1153 insertions(+), 14 deletions(-) create mode 100644 src/modules/punk/args/moduledoc/punkboot-999999.0a1.0.tm create mode 100644 src/modules/punk/args/moduledoc/punkboot-buildversion.txt create mode 100644 src/modules/punk/buildinfo-999999.0a1.0.tm create mode 100644 src/modules/punk/buildinfo-buildversion.txt create mode 100644 src/tests/modules/punk/buildinfo/testsuites/buildinfo/report.test create mode 100644 src/tests/shell/testsuites/punkexe/buildinfo.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 27605682..b8878912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ 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` "Project Versioning" section for the bump policy. +## [0.47.0] - 2026-08-02 + +- Punk executables self-report build provenance (G-025): new 'buildinfo' + launch subcommand prints a machine-parseable key=value report on stdout and + exits 0 - the build STAMP baked into the kit (new punkkit-stamp.toml at the + payload root: project version, source vfs folder, input runtime + its + G-117 build_id when recorded, kit/target names, boot-core version - + deliberately no time-of-bake field, so identical inputs stamp + identically), LIVE facts (real executable path, patchlevel, payload base, package + modes), the governing punkproject.toml version when findable + (live_vs_stamp=same|differs), and the inherited runtime-layer + punkbin-artifact.toml record. Identity comes from embedded data: a renamed + or copied kit keeps reporting its stamp. Single implementation is the new + punk::buildinfo module (0.1.0, dependency-free - works in the repl code + interp and tclsh-hosted shells); the bare 'buildinfo' shell command is an + aliascore alias (0.2.0) preloaded by punk::repl (0.5.4); unstamped contexts + report stamp_present=0 rather than fabricating. Boot core 0.2.0 adds the + buildinfo builtin and publishes ::punkboot::boot_state (core_version, + main_script, package_modes, proj_scope, proj_root, kit_payload_base, + src_project_root) - the single boot running-state surface G-089 + anticipated. punk::args::moduledoc::punkexe 0.3.0 documents the + subcommand; new punk::args::moduledoc::punkboot 0.1.0 documents the G-031 + boot-core API (thin-main contract, registration model, persistent + ::punkboot procs) runtime-queryably. + ## [0.46.0] - 2026-08-02 - Generated projects get the componentized boot + its guidance (G-031 diff --git a/punkproject.toml b/punkproject.toml index efa110d7..30418252 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.46.0" +version = "0.47.0" license = "BSD-2-Clause" url = "https://www.gitea1.intx.com.au/jn/punkshell" #packager: declared identity for published artifacts (declarative, not proof - diff --git a/src/bootsupport/modules/include_modules.config b/src/bootsupport/modules/include_modules.config index 95c2f4dc..223df580 100644 --- a/src/bootsupport/modules/include_modules.config +++ b/src/bootsupport/modules/include_modules.config @@ -64,6 +64,7 @@ set bootsupport_modules [list\ modules punkcheck\ modules punkcheck::cli\ modules punk::aliascore\ + modules punk::buildinfo\ modules punk::ansi::colourmap\ modules punk::ansi\ modules punk::ansi::sauce\ diff --git a/src/make.tcl b/src/make.tcl index 6839eff7..26b87268 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -9031,6 +9031,73 @@ foreach vfstail $vfs_tails { continue } + #G-025 kit stamp: build provenance embedded at the payload root so the + #built executable answers for itself whatever it is later named. Read + #by punk::buildinfo (kit 'buildinfo' subcommand / in-shell command); + #a distinct namespaced file beside the runtime layer's own + #punkbin-artifact.toml record (G-117), which is read alongside it. + #Written AFTER the boot gate (assembled tree final) and BEFORE image + #assembly; utc_bake only changes when the kit actually reassembles. + apply {{targetvfs projectroot vfstail runtime_fullname targetkit rt_target} { + set stamp_project_version "" + catch { + if {![catch {package require punkboot::utils}]} { + set stamp_project_version [::punkboot::utils::read_punkproject_version [file join $projectroot punkproject.toml]] + } + } + set core_ver "" + set corefile [file join $targetvfs punkboot core.tcl] + if {[file isfile $corefile]} { + set fd [open $corefile r]; set coredata [read $fd]; close $fd + regexp {variable\s+core_version\s+(\S+)} $coredata -> core_ver + } + set rr_name "" + set rr_build_id "" + set rrfile [file join $targetvfs punkbin-artifact.toml] + if {[file isfile $rrfile]} { + set fd [open $rrfile r]; set rrdata [read $fd]; close $fd + set insection 0 + foreach line [split $rrdata \n] { + set trimmed [string trim $line] + if {[string index $trimmed 0] eq "\["} { + set insection [expr {$trimmed eq "\[artifact\]"}] + continue + } + if {!$insection} {continue} + regexp {^name\s*=\s*"([^"]*)"} $trimmed -> rr_name + regexp {^build_id\s*=\s*"([^"]*)"} $trimmed -> rr_build_id + } + } + set lines [list] + lappend lines "#punkshell kit build stamp (G-025 schema 1) - written by 'make.tcl bake'." + lappend lines "#Provenance of THIS kit assembly, read by punk::buildinfo; the input" + lappend lines "#runtime layer's own record is the sibling punkbin-artifact.toml (G-117)." + lappend lines "schema = 1" + lappend lines "" + lappend lines "\[punkkit\]" + lappend lines "project_version = \"$stamp_project_version\"" + lappend lines "vfs = \"$vfstail\"" + lappend lines "runtime = \"$runtime_fullname\"" + if {$rr_build_id ne ""} { + lappend lines "runtime_build_id = \"$rr_build_id\"" + } + if {$rr_name ne ""} { + lappend lines "runtime_artifact = \"$rr_name\"" + } + lappend lines "kit = \"$targetkit\"" + lappend lines "target = \"$rt_target\"" + lappend lines "boot_core_version = \"$core_ver\"" + #NO time-of-bake field: the target tree is clean-slate reassembled + #every bake, so a volatile field would make otherwise-identical + #stamps (and kits) differ run to run. Same principle as the G-117 + #embedded records, which keep sha1/size/built in the SIDECAR only - + #time facts belong to punkcheck records and sidecars, not payloads. + set fd [open [file join $targetvfs punkkit-stamp.toml] w] + puts -nonewline $fd "[join $lines \n]\n" + close $fd + puts stdout " kit stamp (G-025): punkkit-stamp.toml (project_version=$stamp_project_version vfs=$vfstail runtime=$runtime_fullname boot_core=$core_ver)" + }} $targetvfs $projectroot $vfstail $runtime_fullname $targetkit $rt_target + #G-057 kit icon step: sidecar for every kit; PE icon embedding where #the target has PE resources and this host can write them. Stub-first: #the assemblers below consume wrap_runtime - the stamped per-kit copy diff --git a/src/modules/punk/aliascore-999999.0a1.0.tm b/src/modules/punk/aliascore-999999.0a1.0.tm index daa42de2..1e84c71f 100644 --- a/src/modules/punk/aliascore-999999.0a1.0.tm +++ b/src/modules/punk/aliascore-999999.0a1.0.tm @@ -107,6 +107,7 @@ tcl::namespace::eval punk::aliascore { # - if they are not - e.g (separately loaded ensemble command ?) # the aliascore::init will temporarily extend the exports list to do the import, and then reset the exports to how they were. set aliases [tcl::dict::create {*}{ + buildinfo ::punk::buildinfo::report val ::punk::pipe::val tstr ::punk::args::lib::tstr FOR ::punk::lib::FOR diff --git a/src/modules/punk/aliascore-buildversion.txt b/src/modules/punk/aliascore-buildversion.txt index f47d01c8..c8081f71 100644 --- a/src/modules/punk/aliascore-buildversion.txt +++ b/src/modules/punk/aliascore-buildversion.txt @@ -1,3 +1,4 @@ -0.1.0 +0.2.0 #First line must be a semantic version number #all other lines are ignored. +#0.2.0 - added buildinfo -> ::punk::buildinfo::report (G-025 in-shell self-report; punk::repl preloads punk::buildinfo before init so the alias target exists) diff --git a/src/modules/punk/args/moduledoc/punkboot-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/punkboot-999999.0a1.0.tm new file mode 100644 index 00000000..cc82a462 --- /dev/null +++ b/src/modules/punk/args/moduledoc/punkboot-999999.0a1.0.tm @@ -0,0 +1,221 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2026 +# +# @@ Meta Begin +# Application punk::args::moduledoc::punkboot 999999.0a1.0 +# Meta platform tcl +# Meta license BSD +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +package require Tcl 8.6- + +#*** !doctools +#[section API] + +tcl::namespace::eval punk::args::moduledoc::punkboot { + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + # punk::args definitions for the G-031 componentized kit boot: the thin + # project-owned main.tcl contract and the layout-owned boot core's + # post-boot ::punkboot surface. Runtime-queryable from any kit carrying + # this moduledoc (delivered via _vfscommon like moduledoc::punkexe). + # The launch surface itself (package modes + subcommands) is documented + # by punk::args::moduledoc::punkexe; THIS package documents the API an + # application developer or probe writes against. + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + #*** !doctools + #[subsection {Namespace punk::args::moduledoc::punkboot}] + #[para] punk::args definitions for the punkshell kit boot core (G-031) + #[list_begin definitions] + + variable PUNKARGS + + namespace eval argdoc { + #Overview + thin-main authoring contract. Display-first: the thin main + #is a file convention, not a command. + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id (script)::punkboot + @cmd -name punkboot\ + -summary\ + "Componentized kit boot: thin main + shared boot core (G-031)."\ + -help\ + "Every punkshell-style kit boots through a THIN project-owned + main.tcl that declares the kit's launch surface and hands over + to the shared, layout-owned boot core - delivered inside the + kit at /punkboot/core.tcl by the _vfscommon merge + (master: src/vfs/_config/punkboot_core.tcl, regenerated by + 'make.tcl vfscommonupdate'; version: ::punkboot::core_version, + also stamped into kits and reported by 'buildinfo' - G-025). + + THIN MAIN OBLIGATIONS (inside 'apply {args {...}}' invoked + from the toplevel, so dispatch 'uplevel 1' reaches the global + level) - publish in ::punkboot before sourcing the core: + launch_args the launch arguments (default: \$::argv) + main_script normalized path of the kit's root + main.tcl - vfs-root derivations key on + its location + project_subcommands optional dict: subcommand name -> + handler script (see the registration + definition id (script)::punkboot::registration) + launch_defaults optional dict: noargs / unknownfirst + then source the core, first hit of: + /punkboot/core.tcl (baked kit) + /punkboot/core.tcl (nested app/main.tcl runtimes) + /punkboot_core.tcl (sibling master - source-tree + execution of the main) + + POST-BOOT SURFACE the core leaves defined for probes, tests + and modules: the ::punkboot procs documented in this package + (zipfs_mount_pairs, zipfs_kit_mountbase, path_within, + proj_root_find) and the variables ::punkboot::core_version, + ::punkboot::boot_state (dict: core_version, main_script, + package_modes, proj_scope, proj_root, kit_payload_base, and - + src mode only - src_project_root), plus the G-058 static + package captures (static_packages, static_prefixes). + + The template thin main with worked examples is + src/vfs/_config/project_main.tcl (seeded into generated + projects); boot-entry placement rules (fauxlink vs plain + main.tcl, one-startup-script collision refusal) are in + src/vfs/README.md." + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + + #Registration model detail (display-first). + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id (script)::punkboot::registration + @cmd -name "punkboot registration"\ + -summary\ + "Thin-main declarations: project subcommands and launch defaults."\ + -help\ + "::punkboot::project_subcommands - dict mapping subcommand + name -> Tcl handler script. Consulted by the boot core's + default dispatch AFTER the built-ins (tclsh shellspy punk + shell script buildinfo): a declared name extends the launch + surface; a name colliding with a built-in is reported on + stderr and ignored (built-ins cannot be shadowed). The + handler runs in the boot frame with ::argv/::argc already + holding the subcommand's arguments - typically a one-line + 'package require app-yourapp'. + + ::punkboot::launch_defaults - dict with optional keys: + noargs subcommand assumed for a bare launch + (default: shell) + unknownfirst 'script' treats a non-subcommand first + argument as a script invocation (default), or + the name of a known subcommand to receive the + whole arglist (tool-style kits) + + Both dicts default to empty: an undeclared thin main yields + exactly the punkshell launch behaviour." + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + + #Persistent ::punkboot procs (live definitions - the procs remain + #defined after boot; G-129 derivations). + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id ::punkboot::zipfs_mount_pairs + @cmd -name ::punkboot::zipfs_mount_pairs\ + -summary\ + "zipfs mount table as a flat mountpoint,archivefile pair list."\ + -help\ + "Returns the zipfs mount table as a flat list of + mountpoint,archivefile pairs - the pairing 'tcl::zipfs::mount' + (no args) reports in every zipfs generation supported as a kit + runtime (8.7/9 core zipfs and the androwish/undroidwish 8.6 + backport; G-129 measured both). Empty list when zipfs is + absent, nothing is mounted, or the table shape is unknown + (callers treat that as 'cannot attribute any mount')." + @form -synopsis "::punkboot::zipfs_mount_pairs" + @values -min 0 -max 0 + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id ::punkboot::zipfs_kit_mountbase + @cmd -name ::punkboot::zipfs_kit_mountbase\ + -summary\ + "Mountpoint of THIS executable's attached zipfs archive, or empty."\ + -help\ + "G-129: where did this executable's attached archive mount? + Returns the mountpoint whose archive file is + [info nameofexecutable] - //zipfs:/app on modern runtimes, + the executable's own path on the 8.6 backport family (whose + archive-file column may drop the windows drive prefix; that + spelling is accepted). Fallback: a mountpoint containing + [info script]. Empty string when no mount can be attributed - + the boot reports that case on stderr and configures no + kit-internal paths." + @form -synopsis "::punkboot::zipfs_kit_mountbase" + @values -min 0 -max 0 + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id ::punkboot::path_within + @cmd -name ::punkboot::path_within\ + -summary\ + "1 if path equals base or lies under it (segment-aware)."\ + -help\ + "Path-segment-aware containment test (windows compares + case-insensitively). Both arguments are expected + pre-normalized with forward slashes. An empty base never + contains anything." + @form -synopsis "::punkboot::path_within path base" + @leaders + path -type string -optional 0 -help\ + "Path to test." + base -type string -optional 0 -help\ + "Candidate containing base path." + @values -min 0 -max 0 + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id ::punkboot::proj_root_find + @cmd -name ::punkboot::proj_root_find\ + -summary\ + "Walk up to the nearest punkshell-style project root, or empty."\ + -help\ + "G-033: lean boot mirror of punk::repo's project discovery + using only Tcl builtins. Walks up from startdir to the nearest + VCS repo root (git or fossil control markers) that also looks + like a punkshell-style project: not an unwise location, has + ./src, and one of ./src/modules, ./src/vfs, ./src/scriptapps + or ./punkproject.toml. Returns the project root or an empty + string. Semantics deliberately aligned with punk::repo so a + project findable at boot is findable by the runtime layer and + vice versa." + @form -synopsis "::punkboot::proj_root_find startdir" + @leaders + startdir -type string -optional 0 -help\ + "Directory to start the walk-up from." + @values -min 0 -max 0 + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::args::moduledoc::punkboot ---}] +} + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::args::moduledoc::punkboot ::punk::args::moduledoc::punkboot::argdoc +} + +## Ready +package provide punk::args::moduledoc::punkboot [tcl::namespace::eval punk::args::moduledoc::punkboot { + variable version + set version 999999.0a1.0 +}] +return diff --git a/src/modules/punk/args/moduledoc/punkboot-buildversion.txt b/src/modules/punk/args/moduledoc/punkboot-buildversion.txt new file mode 100644 index 00000000..8aed6bdc --- /dev/null +++ b/src/modules/punk/args/moduledoc/punkboot-buildversion.txt @@ -0,0 +1,4 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. +#0.1.0 - initial (G-031 runtime-queryable boot-core API doc): (script)::punkboot overview + thin-main obligations, (script)::punkboot::registration (project_subcommands / launch_defaults), live definitions for the persistent ::punkboot procs (zipfs_mount_pairs, zipfs_kit_mountbase, path_within, proj_root_find); boot_state surface documented diff --git a/src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm index de5ee9af..410f3200 100644 --- a/src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm +++ b/src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm @@ -133,7 +133,7 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { ties (faithful to the project's snapshot vintage) The launch reports the detected project root and effective precedence." - subcommand -type string -optional 1 -choicerestricted 0 -choices {tclsh script shell punk shellspy} -choicelabels { + subcommand -type string -optional 1 -choicerestricted 0 -choices {tclsh script shell punk shellspy buildinfo} -choicelabels { tclsh " Run as a (near) stock tclsh - no punk modules loaded." script @@ -144,6 +144,8 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { " punk shell launch; piped-stdin friendly (echo cmds | punk)." shellspy " Experimental shellspy command-line processor." + buildinfo + " Print the kit's build/identity stamp report and exit (G-025)." } -help\ "Subcommand to run. Each subcommand's own contract is documented under its definition id (script)::punkexe::." @@ -343,6 +345,43 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { "Arguments for the shellspy processor." }] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + + #buildinfo subcommand: G-025 self-report. The single implementation is + #punk::buildinfo (the subcommand is a thin wrapper over its main proc); + #the same-named in-shell command is an aliascore alias to + #punk::buildinfo::report, so kit shells and tclsh-hosted shells + #(tclsh src/make.tcl shell) report identically in-process. + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + lappend PUNKARGS [list { + @id -id (script)::punkexe::buildinfo + @cmd -name "punkexe buildinfo"\ + -summary\ + "Print the kit's build/identity report and exit (G-025)."\ + -help\ + "Print a machine-parseable build/identity report on stdout and + exit 0 - no repl fallthrough, nothing else on stdout, so the + output is usable by exec-style and piped callers. + + One key=value pair per line (split each line on the FIRST + '='). Three notions are kept distinct and never fabricated: + the build STAMP baked into the kit (punkkit-stamp.toml: + project version, source vfs folder, input runtime and its + G-117 build_id when recorded, kit/target names, boot-core + version - correct however the executable file has + since been named), LIVE facts (actual executable path, + patchlevel, payload base, package modes), and the governing + source tree's punkproject.toml version when one is findable + (with live_vs_stamp=same|differs when both are present - a + src-mode session can truthfully differ from its stamp). + stamp_present=0 marks an unstamped kit; fields of absent + notions are omitted rather than invented. + + In-process equivalent (no exec): the 'buildinfo' shell command + / punk::buildinfo::report." + @form -synopsis " buildinfo" + @values -min 0 -max 0 + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- } #*** !doctools diff --git a/src/modules/punk/args/moduledoc/punkexe-buildversion.txt b/src/modules/punk/args/moduledoc/punkexe-buildversion.txt index 47608ceb..7b3f30bb 100644 --- a/src/modules/punk/args/moduledoc/punkexe-buildversion.txt +++ b/src/modules/punk/args/moduledoc/punkexe-buildversion.txt @@ -1,4 +1,5 @@ -0.2.0 +0.3.0 #First line must be a semantic version number #all other lines are ignored. +#0.3.0 - buildinfo subcommand documented (G-025): (script)::punkexe::buildinfo definition + choices entry; stamp/live/source distinction + in-shell equivalent noted #0.2.0 - package_mode 'dev' renamed 'minted' (clean break, no alias - stage-true vocabulary follow-on to G-155): mode docs updated; the built-output mode now names what it consumes (mint output trees) diff --git a/src/modules/punk/buildinfo-999999.0a1.0.tm b/src/modules/punk/buildinfo-999999.0a1.0.tm new file mode 100644 index 00000000..78e12df0 --- /dev/null +++ b/src/modules/punk/buildinfo-999999.0a1.0.tm @@ -0,0 +1,334 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2026 +# +# @@ Meta Begin +# Application punk::buildinfo 999999.0a1.0 +# Meta platform tcl +# Meta license BSD +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +## Deliberately NONE beyond core Tcl: this module must load and answer in any +## context - kit main interp, repl code interp, tclsh-hosted punk shell +## (tclsh src/make.tcl shell), plain tclsh with punk modules on the path. +package require Tcl 8.6- + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#G-025: punk executables self-report project version and build provenance. +#This module is the SINGLE IMPLEMENTATION - the kit launch subcommand +#'buildinfo' (boot core dispatch) is a thin wrapper over punk::buildinfo::main, +#and the bare 'buildinfo' shell command is an aliascore alias to +#punk::buildinfo::report. Three notions kept distinct, never fabricated: +# 1. build stamp - punkkit-stamp.toml at the kit payload base, written by +# 'make.tcl bake' (project version, vfs folder, runtime, +# kit/target names, boot-core version, bake utc) +# 2. live facts - info nameofexecutable / patchlevel / derived payload base +# 3. source tree - punkproject.toml version of the governing project, when +# one is findable (boot-published root, exe-relative, or +# cwd walk-up) +#Also read: the G-117 runtime-layer record punkbin-artifact.toml inherited +#from the input runtime's attached image (schema v1/v2 [artifact] section). +namespace eval punk::buildinfo { + namespace export report get_dict main + variable stamp_filename punkkit-stamp.toml + variable runtime_record_filename punkbin-artifact.toml + + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::punk::buildinfo::get_dict + @cmd -name "::punk::buildinfo::get_dict"\ + -summary\ + "Build/identity facts as a nested dict (stamp, live, source, runtime_record)"\ + -help\ + "Returns a dict with keys: + stamp dict: present 0|1 + the punkkit-stamp.toml + fields when present (project_version, vfs, + runtime, runtime_build_id, kit, target, + boot_core_version - deliberately no + time-of-bake field: stamps are + deterministic for identical inputs) + live dict: executable, patchlevel, payload_base, + and (when the boot core published state) + package_modes / boot_core_version + source dict: present 0|1 + project_root and + project_version when a governing + punkproject.toml was found + runtime_record dict: present 0|1 + name/build_id/revision + from the runtime-layer punkbin-artifact.toml + inherited in the kit payload (G-117) + Nothing is fabricated: absent notions report present 0 and + omit their fields." + }] + lappend PUNKARGS [list { + @id -id ::punk::buildinfo::report + @cmd -name "::punk::buildinfo::report"\ + -summary\ + "Machine-parseable key=value build/identity report (G-025)"\ + -help\ + "Returns the get_dict facts flattened to one key=value pair + per line (split each line on the FIRST '=' - values may + contain '='). stamp_present is always emitted; stamp_*, + live_project_*, runtime_record_* lines appear only when + their notion is present, so absence is explicit rather + than fabricated. The kit launch subcommand 'buildinfo' + prints exactly this report on stdout and exits 0." + }] + lappend PUNKARGS [list { + @id -id ::punk::buildinfo::main + @cmd -name "::punk::buildinfo::main"\ + -summary\ + "Print the report to stdout; returns 0 (launch subcommand body)"\ + -help\ + "Prints punk::buildinfo::report to stdout with no other + output and returns 0 - the boot core's 'buildinfo' + subcommand exits with that value (G-015-compatible: + machine-parseable stdout, honest exit code, no repl + fallthrough)." + }] + } + + #minimal line reader for OUR OWN controlled flat files (punkkit-stamp.toml, + #punkbin-artifact.toml): [section] tracking + key = "quoted" | bare values. + #NOT a general TOML parser - the punkshell TOML surface is tomlish; these + #records are schema-controlled here precisely so boot-adjacent code needs + #no package. + proc toml_section_kv {tomlfile section} { + set d [dict create] + if {![file isfile $tomlfile]} { + return $d + } + set fd [open $tomlfile r] + try { + set data [read $fd] + } finally { + close $fd + } + set insection 0 + foreach line [split $data \n] { + set trimmed [string trim $line] + if {[string index $trimmed 0] eq "\["} { + set insection [expr {$trimmed eq "\[$section\]"}] + continue + } + if {!$insection} {continue} + if {[regexp {^([A-Za-z0-9_]+)\s*=\s*"([^"]*)"} $trimmed -> k v]} { + dict set d $k $v + } elseif {[regexp {^([A-Za-z0-9_]+)\s*=\s*(\S+)} $trimmed -> k v]} { + dict set d $k $v + } + } + return $d + } + + #the running kit's payload base ("" when not a kit / not attributable). + #Boot-published state wins (the boot core derived it authoritatively); + #self-derivation is a lite mirror of the core's zipfs_kit_mountbase + #(authority: src/vfs/_config/punkboot_core.tcl - keep in sync) for + #contexts without ::punkboot (code interp, plain tclsh). + proc payload_base {} { + if {[info exists ::punkboot::boot_state] + && [dict exists $::punkboot::boot_state kit_payload_base]} { + return [dict get $::punkboot::boot_state kit_payload_base] + } + if {[info commands ::tcl::zipfs::mount] ne "" + && ![catch {::tcl::zipfs::mount} mtable] + && [llength $mtable] % 2 == 0 && [llength $mtable]} { + set normexe [file dirname [file normalize [file join [info nameofexecutable] __dummy__]]] + set on_windows [expr {"windows" eq $::tcl_platform(platform)}] + if {$on_windows} { + set exe_forms [list [string tolower $normexe]] + if {[string match {[a-zA-Z]:/*} $normexe]} { + #8.6 backport quirk: archive-file column may drop the drive prefix + lappend exe_forms [string tolower [string range $normexe 2 end]] + } + } else { + set exe_forms [list $normexe] + } + foreach {mountpoint archivefile} $mtable { + set f [string map [list \\ /] $archivefile] + if {$on_windows} { + set f [string tolower $f] + } + if {$f in $exe_forms} { + return $mountpoint + } + } + } + if {[info exists ::tcl::kitpath] && $::tcl::kitpath ne ""} { + return $::tcl::kitpath + } + if {"//cookit:/" in [file volumes] && [file exists //cookit:/lib]} { + return //cookit:/ + } + return "" + } + + #the governing project root for LIVE punkproject.toml reporting ("" when + #none): boot-published roots win (proj: scope, then src mode), else the + #executable's own project (bin/ convention), else a cwd walk-up. The test + #here is punkproject.toml presence only - live version reporting has no + #VCS-marker requirement (an exported tree still has a version). + proc project_root {} { + if {[info exists ::punkboot::boot_state]} { + foreach k {proj_root src_project_root} { + if {[dict exists $::punkboot::boot_state $k]} { + set r [dict get $::punkboot::boot_state $k] + if {$r ne "" && [file isfile [file join $r punkproject.toml]]} { + return $r + } + } + } + } + set normexe [file dirname [file normalize [file join [info nameofexecutable] __dummy__]]] + if {[file tail [file dirname $normexe]] eq "bin"} { + set cand [file dirname [file dirname $normexe]] + } else { + set cand [file dirname $normexe] + } + if {[file isfile [file join $cand punkproject.toml]]} { + return $cand + } + set unwise [list "/" "c:/" "c:/windows"] + set dir [pwd] + while 1 { + if {[string tolower $dir] ni $unwise && [file isfile [file join $dir punkproject.toml]]} { + return $dir + } + set parent [file dirname $dir] + if {$parent eq $dir} { + return "" + } + set dir $parent + } + } + + #read the [project] version from a punkproject.toml (same lite scan as + #punkboot::utils::parse_punkproject_version - kept local so this module + #stays dependency-free; punkboot::utils is not present in all contexts) + proc project_version_read {tomlfile} { + set d [toml_section_kv $tomlfile project] + if {[dict exists $d version]} { + return [dict get $d version] + } + return "" + } + + proc get_dict {} { + variable stamp_filename + variable runtime_record_filename + set base [payload_base] + + set stamp [dict create present 0] + if {$base ne ""} { + set sd [toml_section_kv [file join $base $stamp_filename] punkkit] + if {[dict size $sd]} { + set stamp [dict merge [dict create present 1] $sd] + } + } + + set live [dict create \ + executable [file normalize [info nameofexecutable]] \ + patchlevel [info patchlevel] \ + payload_base $base \ + ] + if {[info exists ::punkboot::boot_state]} { + if {[dict exists $::punkboot::boot_state package_modes]} { + dict set live package_modes [dict get $::punkboot::boot_state package_modes] + } + } + if {[info exists ::punkboot::core_version]} { + dict set live boot_core_version $::punkboot::core_version + } + + set source [dict create present 0] + set proot [project_root] + if {$proot ne ""} { + set pver [project_version_read [file join $proot punkproject.toml]] + if {$pver ne ""} { + set source [dict create present 1 project_root $proot project_version $pver] + } + } + + set rrec [dict create present 0] + if {$base ne ""} { + set rd [toml_section_kv [file join $base $runtime_record_filename] artifact] + if {[dict size $rd]} { + set rrec [dict create present 1] + foreach k {name build_id revision target} { + if {[dict exists $rd $k]} { + dict set rrec $k [dict get $rd $k] + } + } + } + } + return [dict create stamp $stamp live $live source $source runtime_record $rrec] + } + + proc report {} { + set d [get_dict] + set lines [list] + set stamp [dict get $d stamp] + lappend lines "stamp_present=[dict get $stamp present]" + if {[dict get $stamp present]} { + foreach k {project_version vfs runtime runtime_build_id runtime_artifact kit target boot_core_version} { + if {[dict exists $stamp $k]} { + lappend lines "stamp_$k=[dict get $stamp $k]" + } + } + } + set live [dict get $d live] + foreach k {executable patchlevel payload_base package_modes boot_core_version} { + if {[dict exists $live $k]} { + lappend lines "live_$k=[dict get $live $k]" + } + } + set source [dict get $d source] + if {[dict get $source present]} { + lappend lines "live_project_root=[dict get $source project_root]" + lappend lines "live_project_version=[dict get $source project_version]" + if {[dict get $stamp present] && [dict exists $stamp project_version]} { + if {[dict get $stamp project_version] eq [dict get $source project_version]} { + lappend lines "live_vs_stamp=same" + } else { + lappend lines "live_vs_stamp=differs" + } + } + } + set rrec [dict get $d runtime_record] + if {[dict get $rrec present]} { + foreach k {name build_id revision} { + if {[dict exists $rrec $k]} { + lappend lines "runtime_record_$k=[dict get $rrec $k]" + } + } + } + return [join $lines \n] + } + + proc main {} { + puts stdout [report] + return 0 + } +} + +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::buildinfo ::punk::buildinfo::argdoc +} + +## Ready +package provide punk::buildinfo [tcl::namespace::eval punk::buildinfo { + variable version + #- this version number, exactly 999999.0a1.0, is a literal used in src module folders + #- we refer to this sometimes as the magic version number + set version 999999.0a1.0 +}] +return diff --git a/src/modules/punk/buildinfo-buildversion.txt b/src/modules/punk/buildinfo-buildversion.txt new file mode 100644 index 00000000..47694417 --- /dev/null +++ b/src/modules/punk/buildinfo-buildversion.txt @@ -0,0 +1,4 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. +#0.1.0 - initial (G-025): single-implementation build/identity report - punkkit-stamp.toml kit stamp, live facts, governing punkproject.toml version, inherited G-117 punkbin-artifact.toml runtime record; report = key=value lines, main = print + return 0 (the boot core's 'buildinfo' subcommand body) diff --git a/src/modules/punk/repl-999999.0a1.0.tm b/src/modules/punk/repl-999999.0a1.0.tm index 6c6de153..eeb2718f 100644 --- a/src/modules/punk/repl-999999.0a1.0.tm +++ b/src/modules/punk/repl-999999.0a1.0.tm @@ -100,6 +100,7 @@ package require shellfilter #package require punk package require punk::lib package require punk::args +package require punk::buildinfo ;#G-025 self-report - loaded before aliascore::init so the 'buildinfo' alias target exists (works identically in kit shells and tclsh-hosted shells) package require punk::aliascore if {[catch {punk::aliascore::init} errM]} { puts stderr "punk::aliascore::init error: $errM" diff --git a/src/modules/punk/repl-buildversion.txt b/src/modules/punk/repl-buildversion.txt index 6907aa46..4ef6f4fd 100644 --- a/src/modules/punk/repl-buildversion.txt +++ b/src/modules/punk/repl-buildversion.txt @@ -1,6 +1,7 @@ -0.5.3 +0.5.4 #First line must be a semantic version number #all other lines are ignored. +#0.5.4 - preload punk::buildinfo before aliascore::init (G-025: bare buildinfo command available in kit shells and tclsh-hosted shells alike) #0.5.3 - code interp setup loads punk::args::moduledoc::punkexe (catch-guarded beside the tclcore moduledoc requires, both code-interp setup sites) so the launch subcommand family documentation ids ((script)::punkexe and (script)::punkexe::tclsh/script/shell/punk/shellspy - G-118 item 10) are queryable via punk::args::usage in any punk shell without a manual package require. Not critical - absence degrades silently like tclcore. #0.5.2 - G-076: repl_handler now handles a dead console DELIVERED as a readable event (runtimes carrying the upstream f10d91c2d3 fix - first such 9.0-line version 9.0.5): the previously unguarded full 'chan configure' probe at handler entry errored ('couldn't read console mode: broken pipe'), backgrounding the error and leaving the repl idle with its reader inactive. The probe is now guarded, and terminal input loss routes through new repl::console_input_lost - the same teardown console_watchdog uses (cancel any watchdog schedule, deregister reader, close channel to stop the core reader thread, finish via the eof done-path; no reopen attempt). console_watchdog now shares this helper (probe unchanged, message unchanged). Verified with the G-039 kill procedure on a zig-built core-9-0-branch (9.0.5) runtime with the watchdog disarmed via the shipping gate: clean exit 0.2s after conhost kill through app-punkshell's no-console eof handling; pre-fix runtimes are unaffected (the defect never delivers the event - the watchdog remains their rescue path). #0.5.2 - fixed the codethread seeding template in repl::start: it namespace-eval'd the full ::punk::nav::ns::ns_current VARIABLE name (creating a stray namespace of that name in every code interp - visible as a phantom 'ns_current' child when browsing the nav namespace with n/) instead of just ensuring the parent ::punk::nav::ns exists before setting the variable. Seeding semantics unchanged (the subshell namespace carry-over: a new codethread's code interp starts in the launching context's ns_current when it exists, else ::). Guarded by the new punk/repl testsuite repl/nscurrent.test (real codethread via repl::init driven with synchronous runscript sends: inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for a missing current namespace, stray-namespace absence, and a source-text guard on the template pending an end-to-end piped subshell test). diff --git a/src/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl b/src/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl index 2140eaf9..98151603 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl +++ b/src/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl @@ -40,8 +40,9 @@ #core also boots a kit when sourced directly as its main.tcl. # ------------------------------------------------------------------------------ namespace eval ::punkboot { - #boot-core identity (G-031; G-025 stamping is the planned reporting vehicle) - variable core_version 0.1.0 + #boot-core identity (G-031; stamped into kits + reported by the G-025 + #buildinfo surfaces) + variable core_version 0.2.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -1479,6 +1480,35 @@ if {![info exists ::punkboot::launch_defaults]} { catch {package require __src_mode_tm_scan__} } + #--------------------------------------------------------------- + #Boot running-state surface (G-031 registration model - the single boot + #"running state" surface G-089 anticipated). Post-boot consumers (e.g + #punk::buildinfo, scriptlib resolution) read facts the boot derived + #instead of re-deriving or guessing. Dict keys: core_version, main_script, + #package_modes, proj_scope, proj_root, kit_payload_base, and (src mode + #only) src_project_root. + #--------------------------------------------------------------- + set boot_payload_base "" + if {$zipkit_mountbase ne ""} { + set boot_payload_base $zipkit_mountbase + } elseif {[info exists ::tcl::kitpath] && $::tcl::kitpath ne ""} { + set boot_payload_base $::tcl::kitpath + } elseif {$has_cookfs_attached} { + set boot_payload_base $cookbase + } + set boot_state_dict [dict create \ + core_version $::punkboot::core_version \ + main_script $::punkboot::main_script \ + package_modes $package_modes \ + proj_scope $proj_scope \ + proj_root $proj_root \ + kit_payload_base $boot_payload_base \ + ] + if {[info exists src_project_root]} { + dict set boot_state_dict src_project_root $src_project_root + } + namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin @@ -1492,7 +1522,7 @@ if {![info exists ::punkboot::launch_defaults]} { # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1675,6 +1705,21 @@ if {![info exists ::punkboot::launch_defaults]} { # package require app-repl #} } + buildinfo { + #G-025: machine-parseable build/identity report. The single + #implementation is punk::buildinfo - this arm is the thin + #exe-subcommand wrapper. Stdout carries only the report; honest + #exit code; no repl fallthrough (G-015-compatible). + if {[llength $subcommand_arglist]} { + puts stderr "buildinfo: takes no arguments" + exit 2 + } + if {[catch {package require punk::buildinfo} errM]} { + puts stderr "buildinfo: the punk::buildinfo module is not available in this kit/context ($errM)" + exit 1 + } + exit [punk::buildinfo::main] + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the diff --git a/src/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl b/src/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl index 2140eaf9..98151603 100644 --- a/src/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl +++ b/src/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl @@ -40,8 +40,9 @@ #core also boots a kit when sourced directly as its main.tcl. # ------------------------------------------------------------------------------ namespace eval ::punkboot { - #boot-core identity (G-031; G-025 stamping is the planned reporting vehicle) - variable core_version 0.1.0 + #boot-core identity (G-031; stamped into kits + reported by the G-025 + #buildinfo surfaces) + variable core_version 0.2.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -1479,6 +1480,35 @@ if {![info exists ::punkboot::launch_defaults]} { catch {package require __src_mode_tm_scan__} } + #--------------------------------------------------------------- + #Boot running-state surface (G-031 registration model - the single boot + #"running state" surface G-089 anticipated). Post-boot consumers (e.g + #punk::buildinfo, scriptlib resolution) read facts the boot derived + #instead of re-deriving or guessing. Dict keys: core_version, main_script, + #package_modes, proj_scope, proj_root, kit_payload_base, and (src mode + #only) src_project_root. + #--------------------------------------------------------------- + set boot_payload_base "" + if {$zipkit_mountbase ne ""} { + set boot_payload_base $zipkit_mountbase + } elseif {[info exists ::tcl::kitpath] && $::tcl::kitpath ne ""} { + set boot_payload_base $::tcl::kitpath + } elseif {$has_cookfs_attached} { + set boot_payload_base $cookbase + } + set boot_state_dict [dict create \ + core_version $::punkboot::core_version \ + main_script $::punkboot::main_script \ + package_modes $package_modes \ + proj_scope $proj_scope \ + proj_root $proj_root \ + kit_payload_base $boot_payload_base \ + ] + if {[info exists src_project_root]} { + dict set boot_state_dict src_project_root $src_project_root + } + namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin @@ -1492,7 +1522,7 @@ if {![info exists ::punkboot::launch_defaults]} { # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1675,6 +1705,21 @@ if {![info exists ::punkboot::launch_defaults]} { # package require app-repl #} } + buildinfo { + #G-025: machine-parseable build/identity report. The single + #implementation is punk::buildinfo - this arm is the thin + #exe-subcommand wrapper. Stdout carries only the report; honest + #exit code; no repl fallthrough (G-015-compatible). + if {[llength $subcommand_arglist]} { + puts stderr "buildinfo: takes no arguments" + exit 2 + } + if {[catch {package require punk::buildinfo} errM]} { + puts stderr "buildinfo: the punk::buildinfo module is not available in this kit/context ($errM)" + exit 1 + } + exit [punk::buildinfo::main] + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the diff --git a/src/tests/modules/punk/buildinfo/testsuites/buildinfo/report.test b/src/tests/modules/punk/buildinfo/testsuites/buildinfo/report.test new file mode 100644 index 00000000..cc98d1ec --- /dev/null +++ b/src/tests/modules/punk/buildinfo/testsuites/buildinfo/report.test @@ -0,0 +1,181 @@ +# -*- tcl -*- +# Tests for punk::buildinfo - the G-025 single-implementation build/identity report: +# - lite [section] key=value reading of the controlled stamp/record files +# - the three notions kept distinct (stamp / live / source) and never fabricated +# - boot_state override paths (payload base + governing project root) - the same +# seams the boot core publishes in a real kit +# - report shape: key=value lines, stamp_present always first, absence = omission +# Directory fixtures only; ::punkboot::boot_state is faked per-test and cleaned up. +# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punk/buildinfo/*** report.test + +package require tcltest +package require punk::buildinfo + +#added 2026-08-02 (agent, G-025) + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable BASE [makeDirectory g025_buildinfo] + + proc writefile {path content} { + file mkdir [file dirname $path] + set fd [open $path w] + puts -nonewline $fd $content + close $fd + } + proc mkkitbase {name stampcontent {rrcontent ""}} { + variable BASE + set root [file join $BASE $name] + file delete -force $root + file mkdir $root + if {$stampcontent ne ""} { + writefile [file join $root punkkit-stamp.toml] $stampcontent + } + if {$rrcontent ne ""} { + writefile [file join $root punkbin-artifact.toml] $rrcontent + } + return $root + } + proc fake_bootstate {pairs} { + namespace eval ::punkboot [list variable boot_state [dict create {*}$pairs]] + } + proc clear_bootstate {} { + catch {unset ::punkboot::boot_state} + } + + variable STAMP1 {#stamp fixture +schema = 1 + +[punkkit] +project_version = "0.46.0" +vfs = "punk9win.vfs" +runtime = "tclsh9.0.2-r2.exe" +runtime_build_id = "1234-abcd" +kit = "punk902z.exe" +target = "win32-x86_64" +boot_core_version = "0.2.0" +utc_bake = "2026-08-02T12:00:00Z" +} + variable RREC1 {#record fixture +schema = 1 + +[artifact] +name = "tclsh9.0.2-punk-r2.exe" +build_id = "1234-abcd" +revision = 2 +} + + variable common { + set result [list] + clear_bootstate + } + + # -- --- --- lite reader --- --- -- + + test buildinfo_toml_lite_quoted_and_bare {section tracking with quoted and bare values; other sections ignored}\ + -setup $common -body { + variable BASE + writefile [file join $BASE lite.toml] "schema = 1\n\[other\]\nkey = \"nope\"\n\[punkkit\]\nqv = \"a b c\"\nbare = 42\n" + set d [punk::buildinfo::toml_section_kv [file join $BASE lite.toml] punkkit] + lappend result [dict get $d qv] [dict get $d bare] [dict exists $d key] [dict exists $d schema] + }\ + -result {{a b c} 42 0 0} + + test buildinfo_toml_lite_missing_file {a missing file yields an empty dict, not an error}\ + -setup $common -body { + variable BASE + dict size [punk::buildinfo::toml_section_kv [file join $BASE no_such.toml] punkkit] + }\ + -result {0} + + # -- --- --- stamp notion --- --- -- + + test buildinfo_stamp_read_via_bootstate {a stamped payload base (published via boot_state) yields the stamp fields}\ + -setup $common -body { + variable STAMP1 + set root [mkkitbase stamped $STAMP1] + fake_bootstate [list kit_payload_base $root] + set stamp [dict get [punk::buildinfo::get_dict] stamp] + lappend result [dict get $stamp present] [dict get $stamp project_version] [dict get $stamp vfs] [dict get $stamp kit] [dict get $stamp boot_core_version] + }\ + -cleanup {clear_bootstate}\ + -result {1 0.46.0 punk9win.vfs punk902z.exe 0.2.0} + + test buildinfo_unstamped_explicit_absence {an unstamped payload base reports stamp present 0 with no stamp fields}\ + -setup $common -body { + set root [mkkitbase unstamped ""] + fake_bootstate [list kit_payload_base $root] + set stamp [dict get [punk::buildinfo::get_dict] stamp] + lappend result [dict get $stamp present] [dict size $stamp] + }\ + -cleanup {clear_bootstate}\ + -result {0 1} + + test buildinfo_runtime_record_inherited {the G-117 runtime record beside the stamp is reported when readable}\ + -setup $common -body { + variable STAMP1 + variable RREC1 + set root [mkkitbase withrecord $STAMP1 $RREC1] + fake_bootstate [list kit_payload_base $root] + set rrec [dict get [punk::buildinfo::get_dict] runtime_record] + lappend result [dict get $rrec present] [dict get $rrec name] [dict get $rrec build_id] [dict get $rrec revision] + }\ + -cleanup {clear_bootstate}\ + -result {1 tclsh9.0.2-punk-r2.exe 1234-abcd 2} + + # -- --- --- source notion + live_vs_stamp --- --- -- + + test buildinfo_source_same_and_differs {governing punkproject.toml version compares against the stamp - same and differs both label truthfully}\ + -setup $common -body { + variable STAMP1 + variable BASE + set root [mkkitbase cmp $STAMP1] + set ptree [file join $BASE ptree] + file delete -force $ptree + writefile [file join $ptree punkproject.toml] "\[project\]\nname = \"demo\"\nversion = \"0.46.0\"\n" + fake_bootstate [list kit_payload_base $root src_project_root $ptree] + set r1 [punk::buildinfo::report] + writefile [file join $ptree punkproject.toml] "\[project\]\nname = \"demo\"\nversion = \"0.47.0\"\n" + set r2 [punk::buildinfo::report] + lappend result [regexp -line {^live_vs_stamp=same$} $r1] + lappend result [regexp -line {^live_vs_stamp=differs$} $r2] + lappend result [regexp -line {^live_project_version=0\.47\.0$} $r2] + }\ + -cleanup {clear_bootstate}\ + -result {1 1 1} + + # -- --- --- report shape --- --- -- + + test buildinfo_report_shape {stamp_present leads; every line is key=value; live facts always present}\ + -setup $common -body { + variable STAMP1 + set root [mkkitbase shape $STAMP1] + fake_bootstate [list kit_payload_base $root] + set r [punk::buildinfo::report] + set lines [split $r \n] + lappend result [lindex $lines 0] + set allkv 1 + foreach l $lines { + if {![regexp {^[a-z_]+=} $l]} {set allkv 0} + } + lappend result $allkv + lappend result [regexp -line {^live_executable=.+$} $r] [regexp -line {^live_patchlevel=.+$} $r] + }\ + -cleanup {clear_bootstate}\ + -result {stamp_present=1 1 1 1} + + test buildinfo_main_returns_zero {main prints the report and returns 0}\ + -setup $common -body { + variable STAMP1 + set root [mkkitbase mainret $STAMP1] + fake_bootstate [list kit_payload_base $root] + set out "" + set rc [punk::buildinfo::main] + lappend result $rc + }\ + -cleanup {clear_bootstate}\ + -result {0} + + cleanupTests +} diff --git a/src/tests/shell/testsuites/punkexe/buildinfo.test b/src/tests/shell/testsuites/punkexe/buildinfo.test new file mode 100644 index 00000000..50150cd7 --- /dev/null +++ b/src/tests/shell/testsuites/punkexe/buildinfo.test @@ -0,0 +1,123 @@ +# -*- tcl -*- +# Characterization of the G-025 buildinfo launch subcommand against a built punk kit: +# - machine-parseable key=value report on stdout, exit 0, NOTHING on stderr +# - stamp fields present on a currently-baked kit (punkkit-stamp.toml written by bake) +# - RENAMED/COPIED executable keeps reporting its stamp (identity from embedded data, +# not the filename) while live_executable tells the truth about the file +# - argument rejection is honest (exit 2, stderr usage, no report) +# Stamped-content tests are constrained on the probed kit actually carrying a stamp +# (kit_stamped) so a stale pre-G-025 bin/ kit skips rather than red-herrings. +# +# Target executable resolved from env(PUNK_SHELL_TEST_EXE), else /bin/punk902z.exe +# then /bin/punkshell902. Skipped (constraint punkexeavailable) if none found. + +package require tcltest + +#added 2026-08-02 (agent, G-025) + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable testdir [file dirname [file normalize [info script]]] + #/src/tests/shell/testsuites/punkexe -> 5 levels up to + 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]}] + + #run the exe with args; returns dict {exitcode stdout stderr} + proc exe_run {exe args} { + set outfile [makeFile "" exe_stdout.txt] + set errfile [makeFile "" exe_stderr.txt] + set exitcode 0 + if {[catch {exec -- $exe {*}$args > $outfile 2> $errfile} _ errdict]} { + set code [dict get $errdict -errorcode] + if {[lindex $code 0] eq "CHILDSTATUS"} { + set exitcode [lindex $code 2] + } else { + set exitcode -1 + } + } + set fd [open $outfile r]; set out [read $fd]; close $fd + set fd [open $errfile r]; set err [read $fd]; close $fd + return [dict create exitcode $exitcode stdout $out stderr $err] + } + + #probe once: does the target kit carry a stamp? (pre-G-025 kits report + #stamp_present=0 or lack the subcommand entirely - those skip stamped tests) + variable kit_stamped 0 + if {[testConstraint punkexeavailable]} { + if {![catch {exe_run $punkexe buildinfo} pr]} { + if {[dict get $pr exitcode] == 0 && [regexp -line {^stamp_present=1$} [dict get $pr stdout]]} { + set kit_stamped 1 + } + } + } + testConstraint kit_stamped $kit_stamped + + test buildinfo_exit0_stdout_only {report on stdout, exit 0, nothing on stderr}\ + -constraints {punkexeavailable kit_stamped} -body { + variable punkexe + set r [exe_run $punkexe buildinfo] + set result [list] + lappend result [dict get $r exitcode] + lappend result [expr {[dict get $r stderr] eq ""}] + lappend result [regexp -line {^stamp_present=1$} [dict get $r stdout]] + set result + }\ + -result {0 1 1} + + test buildinfo_stamp_fields {stamped kit reports project version, vfs, runtime, kit, target and boot-core version}\ + -constraints {punkexeavailable kit_stamped} -body { + variable punkexe + set out [dict get [exe_run $punkexe buildinfo] stdout] + set result [list] + foreach k {stamp_project_version stamp_vfs stamp_runtime stamp_kit stamp_target stamp_boot_core_version live_executable live_patchlevel} { + lappend result [regexp -line "^$k=.+\$" $out] + } + set result + }\ + -result {1 1 1 1 1 1 1 1} + + test buildinfo_renamed_copy_reports_stamp {a renamed COPY still reports its embedded stamp; live_executable reports the real file}\ + -constraints {punkexeavailable kit_stamped} -body { + variable punkexe + set scratchdir [makeDirectory g025_renamed] + set renamed [file join $scratchdir totally_other_name[file extension $punkexe]] + file copy -force $punkexe $renamed + set orig [dict get [exe_run $punkexe buildinfo] stdout] + set ren [dict get [exe_run $renamed buildinfo] stdout] + regexp -line {^stamp_kit=(.*)$} $orig -> orig_kit + set result [list] + lappend result [regexp -line {^stamp_present=1$} $ren] + lappend result [regexp -line "^stamp_kit=$orig_kit\$" $ren] + lappend result [regexp -line {^live_executable=.*totally_other_name.*$} $ren] + file delete -force $renamed + set result + }\ + -result {1 1 1} + + test buildinfo_rejects_arguments {arguments are rejected honestly: exit 2, stderr usage, no report on stdout}\ + -constraints {punkexeavailable kit_stamped} -body { + variable punkexe + set r [exe_run $punkexe buildinfo extra] + set result [list] + lappend result [dict get $r exitcode] + lappend result [string match "*takes no arguments*" [dict get $r stderr]] + lappend result [expr {[dict get $r stdout] eq ""}] + set result + }\ + -result {2 1 1} + + cleanupTests +} diff --git a/src/vfs/_config/punkboot_core.tcl b/src/vfs/_config/punkboot_core.tcl index 2140eaf9..98151603 100644 --- a/src/vfs/_config/punkboot_core.tcl +++ b/src/vfs/_config/punkboot_core.tcl @@ -40,8 +40,9 @@ #core also boots a kit when sourced directly as its main.tcl. # ------------------------------------------------------------------------------ namespace eval ::punkboot { - #boot-core identity (G-031; G-025 stamping is the planned reporting vehicle) - variable core_version 0.1.0 + #boot-core identity (G-031; stamped into kits + reported by the G-025 + #buildinfo surfaces) + variable core_version 0.2.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -1479,6 +1480,35 @@ if {![info exists ::punkboot::launch_defaults]} { catch {package require __src_mode_tm_scan__} } + #--------------------------------------------------------------- + #Boot running-state surface (G-031 registration model - the single boot + #"running state" surface G-089 anticipated). Post-boot consumers (e.g + #punk::buildinfo, scriptlib resolution) read facts the boot derived + #instead of re-deriving or guessing. Dict keys: core_version, main_script, + #package_modes, proj_scope, proj_root, kit_payload_base, and (src mode + #only) src_project_root. + #--------------------------------------------------------------- + set boot_payload_base "" + if {$zipkit_mountbase ne ""} { + set boot_payload_base $zipkit_mountbase + } elseif {[info exists ::tcl::kitpath] && $::tcl::kitpath ne ""} { + set boot_payload_base $::tcl::kitpath + } elseif {$has_cookfs_attached} { + set boot_payload_base $cookbase + } + set boot_state_dict [dict create \ + core_version $::punkboot::core_version \ + main_script $::punkboot::main_script \ + package_modes $package_modes \ + proj_scope $proj_scope \ + proj_root $proj_root \ + kit_payload_base $boot_payload_base \ + ] + if {[info exists src_project_root]} { + dict set boot_state_dict src_project_root $src_project_root + } + namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin @@ -1492,7 +1522,7 @@ if {![info exists ::punkboot::launch_defaults]} { # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1675,6 +1705,21 @@ if {![info exists ::punkboot::launch_defaults]} { # package require app-repl #} } + buildinfo { + #G-025: machine-parseable build/identity report. The single + #implementation is punk::buildinfo - this arm is the thin + #exe-subcommand wrapper. Stdout carries only the report; honest + #exit code; no repl fallthrough (G-015-compatible). + if {[llength $subcommand_arglist]} { + puts stderr "buildinfo: takes no arguments" + exit 2 + } + if {[catch {package require punk::buildinfo} errM]} { + puts stderr "buildinfo: the punk::buildinfo module is not available in this kit/context ($errM)" + exit 1 + } + exit [punk::buildinfo::main] + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the