You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
12 KiB
247 lines
12 KiB
#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): <workingname minus .exe>-r<N>.exe - |
|
#immutable punkbin names; the -r<N> assembly revision comes from the invocation |
|
#(-rev, suite option -Dfamilyrev). The emitted tree mirrors a punkbin platform |
|
#folder: <outdir>/<artifact>.exe + <artifact>.toml + sha1sums.txt (punkbin |
|
#format: '<sha1> *<filename>'). 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 <dir> -rev <N> -target <punkbin platform, e.g win32-x86_64> |
|
# -suite <name> -tclpatch <patchlevel> -zig <version> -optimize <mode> |
|
# -components {<name> <ver> ...} (attached battery versions, all variants) |
|
# -bicomponents {<name> <ver> ...} (additional bi-only batteries) |
|
# -provenance {<name> <uuid> ...} (source checkout uuids) |
|
# -testreports <dir> (G-107 evidence summaries; optional) |
|
# -kits {<variant> <workingexepath> ...} |
|
|
|
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
|
|
|