#family_artifacts.tcl (G-103; schema v2 per G-117 lineage + G-123 provenance #class): emit punkbin-layout artifact #copies + per-artifact toml metadata + sha1sums for the verified runtime kit #family members. The sidecar's identity fields (schema, build_id, origin, #packager, project, license, build_host_platform) are DERIVED from the record #embedded in each kit's attached image at staging (read back by executing the #kit) - single source of truth; the sidecar adds the finished-binary facts #(sha1, size, built) per the embed-then-hash ordering. # #Deliberately run UNDER THE PLAIN FAMILY KIT itself (not the builder shell): the #sha1 digests come from the kit's attached tcllib/tcllibc, so every emission run #doubles as a proof that the family runtime executes real tooling self-contained. # #Artifact tier naming (G-103 naming decision): -r.exe - #immutable punkbin names; the -r assembly revision comes from the invocation #(-rev, suite option -Dfamilyrev). The emitted tree mirrors a punkbin platform #folder: /.exe + .toml + sha1sums.txt (punkbin #format: ' *'). Publication to the real punkbin repo is a #deliberate user step (copy + build_sha1sums.tcl there); it is DEFERRED per the #goal notes until the family shape is accepted. # #args: -outdir -rev -target # -suite -tclpatch -zig -optimize # -components { ...} (attached battery versions, all variants) # -bicomponents { ...} (additional bi-only batteries) # -provenance { ...} (source checkout uuids) # -testreports (G-107 evidence summaries; optional) # -kits { ...} proc fail {msg} {puts stderr "family_artifacts FAIL: $msg"; flush stderr; exit 1} proc note {msg} {puts stdout "family_artifacts: $msg"; flush stdout} array set opt { -outdir {} -rev 1 -target {} -suite {} -tclpatch {} -zig {} -optimize {} -components {} -bicomponents {} -provenance {} -testreports {} -kits {} } foreach {k v} $argv { if {![info exists opt($k)]} {fail "unknown option '$k'"} set opt($k) $v } foreach req {-outdir -target -suite -tclpatch -zig -optimize -components -kits} { if {$opt($req) eq ""} {fail "missing required option $req"} } if {![string is integer -strict $opt(-rev)] || $opt(-rev) < 1} {fail "-rev must be a positive integer"} if {[catch {package require sha1} sha1ver]} { fail "package require sha1 failed under [info nameofexecutable] - the family kit must carry tcllib: $sha1ver" } proc toml_str {s} { #basic toml string: escape backslash and double-quote (values here are names, #versions, uuids, iso dates - no control chars expected) return "\"[string map {\\ \\\\ \" \\\"} $s]\"" } proc exe_split {name} { #{root ext} splitting only a .exe suffix - dotted tcl patchlevels make #[file rootname] wrong for extensionless (unix) artifact names #(tclsh9.0.5-punk-r1 would truncate at the last version dot) if {[string match -nocase "*.exe" $name]} { return [list [string range $name 0 end-4] .exe] } return [list $name ""] } proc read_embedded {kitpath} { #G-117: exec the kit itself with a piped reader script - its attached image #mounts at //zipfs:/app, so the embedded record is a plain file read. Trusted #just-built kits only; identifying STRAY binaries without execution is the #punk-runtime 'info' zip-central-directory read. set reader { fconfigure stdout -translation binary if {[catch { set f [open //zipfs:/app/punkbin-artifact.toml r] fconfigure $f -translation binary puts -nonewline [read $f] close $f } errM]} { puts -nonewline "PUNKBIN-EMBED-MISSING: $errM" } exit 0 } if {[catch {exec $kitpath << $reader} out]} { fail "embedded-record read failed for $kitpath: $out" } if {[string match "PUNKBIN-EMBED-MISSING*" $out]} { fail "no embedded record in $kitpath ([string range $out 0 200]) - kit predates the G-117 staging embed?" } return [string map [list \r ""] $out] } proc record_field {recordtext key} { #line-based tolerant lookup (same consumer style as punk-runtime): quoted #string or bare integer values; '#' comment lines never match foreach line [split $recordtext \n] { set line [string trim $line] if {[regexp -- [format {^%s\s*=\s*"(.*)"\s*$} $key] $line -> v]} {return $v} if {[regexp -- [format {^%s\s*=\s*([0-9]+)\s*$} $key] $line -> v]} {return $v} } return "" } file mkdir $opt(-outdir) set built [clock format [clock seconds] -format %Y-%m-%dT%H:%M:%SZ -timezone :UTC] set sha1lines {} set emitted {} foreach {variant kitpath} $opt(-kits) { set kitpath [file normalize $kitpath] if {![file exists $kitpath]} {fail "kit exe not found: $kitpath"} set working [file tail $kitpath] lassign [exe_split $working] wroot wext set artifact "$wroot-r$opt(-rev)$wext" set dest [file join $opt(-outdir) $artifact] file delete -force $dest file copy $kitpath $dest set sha1 [sha1::sha1 -hex -file $dest] set size [file size $dest] lappend sha1lines "$sha1 *$artifact" set components $opt(-components) if {$variant eq "punk-bi"} {lappend components {*}$opt(-bicomponents)} set batteries {} foreach {n v} $components {lappend batteries [toml_str "$n $v"]} set batteries_line "attached_batteries = \[[join $batteries {, }]\]" #G-117: read the EMBEDDED record back out of the finished kit and derive the #sidecar's shared identity fields from it - single source of truth, identical #build_id by construction; every emission inherently verifies the embed exists #and matches this invocation (stale-staging tripwire). set embedded [read_embedded $kitpath] foreach {ekey expect} [list schema 2 name $artifact variant $variant working_name $working revision $opt(-rev) target $opt(-target) tcl_patchlevel $opt(-tclpatch)] { set got [record_field $embedded $ekey] if {$got ne $expect} {fail "$artifact embedded record mismatch: $ekey = '$got', expected '$expect' (stale staging?)"} } if {[string first $batteries_line $embedded] < 0} { fail "$artifact embedded record attached_batteries does not match the emission components" } set e_build_id [record_field $embedded build_id] if {$e_build_id eq ""} {fail "$artifact embedded record has no build_id"} set evars [dict create] foreach k {origin packager project project_url license build_host_platform} { set v [record_field $embedded $k] if {$v eq ""} {fail "$artifact embedded record missing v1 field '$k'"} dict set evars $k $v } set m {} lappend m "#punkshell runtime artifact metadata (schema v2, class \"runtime\") - generated by" lappend m "#family_artifacts.tcl. Sidecar copy: the embedded record carried inside the" lappend m "#artifact's attached image (punkbin-artifact.toml) plus the finished-binary" lappend m "#facts (sha1, size, built). The sidecar + sha1sums.txt remain the integrity" lappend m "#authority." lappend m "schema = 2" lappend m "" lappend m "\[artifact\]" lappend m "name = [toml_str $artifact]" lappend m "class = \"runtime\"" lappend m "variant = [toml_str $variant]" lappend m "working_name = [toml_str $working]" lappend m "revision = $opt(-rev)" lappend m "target = [toml_str $opt(-target)]" lappend m "sha1 = [toml_str $sha1]" lappend m "size = $size" lappend m "built = [toml_str $built]" lappend m "#build_id: offline correlation key re-joining a renamed copy to its record;" lappend m "#identical in the embedded and sidecar copies (a deterministic identity digest," lappend m "#not an integrity key - sha1 above is the integrity fact)." lappend m "build_id = [toml_str $e_build_id]" lappend m "#origin: canonical artifact repo this artifact was BUILT FOR - not necessarily" lappend m "#where it is hosted; mirrors preserve it." lappend m "origin = [toml_str [dict get $evars origin]]" lappend m "#packager: declared identity, not proof - signing (minisign sidecars) is the" lappend m "#verification layer." lappend m "packager = [toml_str [dict get $evars packager]]" lappend m "project = [toml_str [dict get $evars project]]" lappend m "project_url = [toml_str [dict get $evars project_url]]" lappend m "#license: summary for the distributed artifact; component license texts ride" lappend m "#inside the attached image (tcl_library/license.terms etc)." lappend m "license = [toml_str [dict get $evars license]]" lappend m "build_host_platform = [toml_str [dict get $evars build_host_platform]]" lappend m "" lappend m "\[runtime\]" lappend m "tcl_patchlevel = [toml_str $opt(-tclpatch)]" set pr [expr {$variant ne "plain"}] lappend m "piperepl = [expr {$pr ? "true" : "false"}]" if {$pr} { lappend m "piperepl_default = \"on\"" lappend m "piperepl_opt_out = \"TCLSH_PIPEREPL=0\"" } lappend m $batteries_line lappend m "" lappend m "\[provenance\]" lappend m "#class: build-origin class (schema v2): suite-built | third-party | local." lappend m "#NOTE for line-based consumers: '\[artifact\] class' above is the first 'class ='" lappend m "#line in the record by construction - whole-text single-key scans see that one." lappend m "class = \"suite-built\"" lappend m "suite = [toml_str $opt(-suite)]" lappend m "toolchain = [toml_str "zig $opt(-zig)"]" lappend m "optimize = [toml_str $opt(-optimize)]" foreach {n uuid} $opt(-provenance) { lappend m "${n}_checkout = [toml_str $uuid]" } #G-107 evidence summaries available at emission time (result lines only; the #full line-record summaries stay the canonical evidence artifacts) if {$opt(-testreports) ne "" && [file isdirectory $opt(-testreports)]} { set tlines {} foreach sf [lsort [glob -nocomplain -directory $opt(-testreports) *.summary]] { set rec [dict create] set f [open $sf r] foreach line [split [read $f] \n] { set line [string trim $line] if {$line eq "" || [string index $line 0] eq "#"} continue if {[catch {llength $line} n] || $n < 2} continue dict set rec [lindex $line 0] [lrange $line 1 end] } close $f if {![dict exists $rec library] || ![dict exists $rec result]} continue set lib [dict get $rec library] set parts [list "result=[dict get $rec result]"] foreach fkey {mode total passed skipped failed} { if {[dict exists $rec $fkey]} {lappend parts "$fkey=[dict get $rec $fkey]"} } lappend tlines "$lib = [toml_str [join $parts { }]]" } if {[llength $tlines]} { lappend m "" lappend m "\[tests\]" lappend m {*}$tlines } } set mf [file join $opt(-outdir) "[lindex [exe_split $artifact] 0].toml"] set f [open $mf w] fconfigure $f -translation lf puts $f [join $m \n] close $f lappend emitted "$variant -> $artifact" note "emitted $artifact (sha1 $sha1, [expr {$size/1024}] KB) + [file tail $mf]" } set f [open [file join $opt(-outdir) sha1sums.txt] w] fconfigure $f -translation lf puts $f [join $sha1lines \n] close $f note "sha1sums.txt written ([llength $sha1lines] artifacts)" puts "family_artifacts OK: [join $emitted {; }] -> $opt(-outdir)" exit 0