Browse Source

make.tcl-generated outputs: punkboot::utils 0.5.0 propagation (bootsupport + _vfscommon) + layout make.tcl sync

Batched punkcheck-managed build outputs from make.tcl modules/bootsupport/
vfscommonupdate for the G-133 change-set: bootsupport and _vfscommon.vfs gain
utils-0.5.0.tm (0.4.0 pruned), thin-layout make.tcl copies and the modpod
templates payload re-synced.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
d2f7dc6f0e
  1. BIN
      src/bootsupport/modules/punk/mix/templates-0.2.0.tm
  2. 352
      src/bootsupport/modules/punkboot/utils-0.4.0.tm
  3. 753
      src/bootsupport/modules/punkboot/utils-0.5.0.tm
  4. 229
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl
  5. 229
      src/project_layouts/vendor/punk/basic/src/make.tcl
  6. 229
      src/project_layouts/vendor/punk/project-0.1/src/make.tcl
  7. BIN
      src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm
  8. 352
      src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.4.0.tm
  9. 753
      src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.5.0.tm

BIN
src/bootsupport/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

352
src/bootsupport/modules/punkboot/utils-0.4.0.tm

@ -1,352 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-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) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.4.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_boot_library_report
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vcs_dirty_warnings
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\
-summary\
"Warning lines for uncommitted VCS changes above a path"\
-help\
"Walks up from path to the nearest fossil and/or git root and
returns warning lines describing uncommitted changes.
Artifacts built from a dirty source tree have no committed
provenance - callers such as make.tcl vendorupdate use this
to flag source projects that should be committed first.
checkedrootsvar names a dict variable in the caller's scope
used to report each VCS root at most once across calls.
label, if supplied, is prefixed into each warning to name
the calling operation (e.g. 'vendorupdate').
scope, if supplied, is a subpath relative to path; only
uncommitted changes under that subpath are counted (e.g.
scope 'src' warns about dirty src/ while ignoring dirt
elsewhere in the checkout).
Returns an empty list if the path is clean, unversioned,
missing, already reported, or state cannot be determined."
@leaders
path -type string -optional 0 -help\
"Path to check; the nearest enclosing fossil/git root is examined"
checkedrootsvar -type string -optional 0 -help\
"Name of a dict variable in the caller's scope for dedupe across calls"
label -type string -optional 1 -default "" -help\
"Operation name prefixed into warnings"
scope -type string -optional 1 -default "" -help\
"Subpath relative to path limiting which changes count; empty = whole checkout"
}]
}
proc vcs_dirty_warnings {path checkedrootsvar {label ""} {scope ""}} {
upvar 1 $checkedrootsvar checkedroots
if {![info exists checkedroots]} {set checkedroots [dict create]}
if {$label ne ""} {set label "$label "}
set warnings [list]
set dir [file normalize $path]
if {![file isdirectory $dir]} {
return $warnings ;#missing path - not this proc's business to report
}
set scopeabs ""
set scopedesc "" ;#included in warning text when scoped
if {$scope ne ""} {
set scopeabs [file normalize [file join $dir $scope]]
set scopedesc " under [string trimright $scope /]/"
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot,$scope] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot,$scope 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} else {
set flines [list]
foreach line [split $fchanges \n] {
set line [string trim $line]
if {$line eq ""} {continue}
if {$scopeabs ne ""} {
#fossil changes lines are '<STATUS> <path-relative-to-checkout-root>'
if {![regexp {^\S+\s+(.*)$} $line _ relfile]} {continue}
set normfile [file normalize [file join $fossilroot $relfile]]
if {$normfile ne $scopeabs && ![string match "${scopeabs}/*" $normfile]} {continue}
}
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot,$scope] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot,$scope 1
set gitpathargs [list]
if {$scopeabs ne ""} {
set gitpathargs [list -- $scopeabs]
}
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain {*}$gitpathargs]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
}
#Companion files of a real Tcl library directory. init.tcl sources these, so a
#directory holding init.tcl with none of them beside it is a package's own init
#script (lib/BWidget1.10.1/init.tcl and friends), not a tcl library.
variable boot_library_companions {tm.tcl package.tcl auto.tcl clock.tcl history.tcl word.tcl}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_boot_library_report
@cmd -name "::punkboot::utils::vfs_boot_library_report"\
-summary\
"Report whether an assembled vfs tree can supply a bootable tcl library"\
-help\
"Structural check of a merged .vfs tree for the precondition a kit
needs to initialise at all: a Tcl library the runtime can find. Reads
the directory only - nothing is executed, decompressed or launched, so
a caller such as make.tcl can run it for every kit on every bake and
for a cross-target kit it could never run itself.
Three conventions are recognised, all observed in real runtimes:
tcl_library/ zipfs-attached kits (tclZipfs looks for
<mount>/tcl_library/init.tcl)
lib/tcl<M>.<m>/ starkit/tclkit-style kits (lib/tcl8.6,
lib/tcl9.0)
tcl<M>.<m>/ runtimes whose archive mounts at the
executable's own path rather than at
//zipfs:/app, so [info library] is
<exe>/tcl8.6 - the androwish/undroidwish
zipfs backport for Tcl 8.6 does this
A candidate directory qualifies only when it holds init.tcl AND at
least one companion file a real Tcl library carries beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or
an encoding/ subdirectory. Without that second test a package's own
init script - lib/BWidget1.10.1/init.tcl, present in every punkshell
kit - would answer for a tcl library it cannot provide.
Returned dict keys:
ok 1 when at least one qualifying library was found
locations qualifying directories, relative to vfsfolder
rejected candidate directories that held init.tcl but no
companion, relative to vfsfolder - the near-misses
worth naming when a gate fires unexpectedly
checked the location patterns examined, relative to vfsfolder
reason why ok is 0; empty string when ok is 1"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to inspect"
}]
}
proc vfs_boot_library_report {vfsfolder} {
variable boot_library_companions
set checked [list tcl_library lib/tcl<M>.<m> tcl<M>.<m>]
set report [dict create ok 0 locations {} rejected {} checked $checked reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report reason "no such directory: $vfsfolder"
return $report
}
#candidates are the three conventions only - a full tree walk would find every
#package's init.tcl and cost more than the check is worth.
#The tcl[0-9]* globs deliberately also match module trees named tcl8/ or tcl9/
#(both exist in this project's kits); they carry no init.tcl, so the qualification
#test below rejects them without needing a narrower pattern.
set candidates [list [file join $vfsfolder tcl_library]]
foreach dir [lsort [glob -nocomplain -type d -directory [file join $vfsfolder lib] -- {tcl[0-9]*}]] {
lappend candidates $dir
}
foreach dir [lsort [glob -nocomplain -type d -directory $vfsfolder -- {tcl[0-9]*}]] {
lappend candidates $dir
}
set locations [list]
set rejected [list]
foreach dir $candidates {
if {![file isfile [file join $dir init.tcl]]} {
continue
}
set qualifies 0
foreach companion $boot_library_companions {
if {[file isfile [file join $dir $companion]]} {
set qualifies 1
break
}
}
if {!$qualifies && [file isdirectory [file join $dir encoding]]} {
set qualifies 1
}
set rel [string range $dir [expr {[string length $vfsfolder] + 1}] end]
if {$qualifies} {
lappend locations $rel
} else {
lappend rejected $rel
}
}
if {[llength $locations]} {
dict set report ok 1
dict set report locations $locations
dict set report rejected $rejected
return $report
}
dict set report rejected $rejected
if {[llength $rejected]} {
dict set report reason "no tcl library in [file tail $vfsfolder]: [join $rejected {, }] hold init.tcl but none of the companion files a tcl library carries beside it ([join $boot_library_companions {, }]) nor an encoding/ directory"
} else {
dict set report reason "no tcl library in [file tail $vfsfolder]: none of tcl_library/init.tcl, lib/tcl<major>.<minor>/init.tcl or tcl<major>.<minor>/init.tcl is present"
}
return $report
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.4.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.4.0
}]
return

753
src/bootsupport/modules/punkboot/utils-0.5.0.tm

@ -0,0 +1,753 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-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) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.5.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export binary_arch_classify parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vcs_dirty_warnings
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\
-summary\
"Warning lines for uncommitted VCS changes above a path"\
-help\
"Walks up from path to the nearest fossil and/or git root and
returns warning lines describing uncommitted changes.
Artifacts built from a dirty source tree have no committed
provenance - callers such as make.tcl vendorupdate use this
to flag source projects that should be committed first.
checkedrootsvar names a dict variable in the caller's scope
used to report each VCS root at most once across calls.
label, if supplied, is prefixed into each warning to name
the calling operation (e.g. 'vendorupdate').
scope, if supplied, is a subpath relative to path; only
uncommitted changes under that subpath are counted (e.g.
scope 'src' warns about dirty src/ while ignoring dirt
elsewhere in the checkout).
Returns an empty list if the path is clean, unversioned,
missing, already reported, or state cannot be determined."
@leaders
path -type string -optional 0 -help\
"Path to check; the nearest enclosing fossil/git root is examined"
checkedrootsvar -type string -optional 0 -help\
"Name of a dict variable in the caller's scope for dedupe across calls"
label -type string -optional 1 -default "" -help\
"Operation name prefixed into warnings"
scope -type string -optional 1 -default "" -help\
"Subpath relative to path limiting which changes count; empty = whole checkout"
}]
}
proc vcs_dirty_warnings {path checkedrootsvar {label ""} {scope ""}} {
upvar 1 $checkedrootsvar checkedroots
if {![info exists checkedroots]} {set checkedroots [dict create]}
if {$label ne ""} {set label "$label "}
set warnings [list]
set dir [file normalize $path]
if {![file isdirectory $dir]} {
return $warnings ;#missing path - not this proc's business to report
}
set scopeabs ""
set scopedesc "" ;#included in warning text when scoped
if {$scope ne ""} {
set scopeabs [file normalize [file join $dir $scope]]
set scopedesc " under [string trimright $scope /]/"
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot,$scope] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot,$scope 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} else {
set flines [list]
foreach line [split $fchanges \n] {
set line [string trim $line]
if {$line eq ""} {continue}
if {$scopeabs ne ""} {
#fossil changes lines are '<STATUS> <path-relative-to-checkout-root>'
if {![regexp {^\S+\s+(.*)$} $line _ relfile]} {continue}
set normfile [file normalize [file join $fossilroot $relfile]]
if {$normfile ne $scopeabs && ![string match "${scopeabs}/*" $normfile]} {continue}
}
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot,$scope] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot,$scope 1
set gitpathargs [list]
if {$scopeabs ne ""} {
set gitpathargs [list -- $scopeabs]
}
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain {*}$gitpathargs]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
}
#Companion files of a real Tcl library directory. init.tcl sources these, so a
#directory holding init.tcl with none of them beside it is a package's own init
#script (lib/BWidget1.10.1/init.tcl and friends), not a tcl library.
variable boot_library_companions {tm.tcl package.tcl auto.tcl clock.tcl history.tcl word.tcl}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_boot_library_report
@cmd -name "::punkboot::utils::vfs_boot_library_report"\
-summary\
"Report whether an assembled vfs tree can supply a bootable tcl library"\
-help\
"Structural check of a merged .vfs tree for the precondition a kit
needs to initialise at all: a Tcl library the runtime can find. Reads
the directory only - nothing is executed, decompressed or launched, so
a caller such as make.tcl can run it for every kit on every bake and
for a cross-target kit it could never run itself.
Three conventions are recognised, all observed in real runtimes:
tcl_library/ zipfs-attached kits (tclZipfs looks for
<mount>/tcl_library/init.tcl)
lib/tcl<M>.<m>/ starkit/tclkit-style kits (lib/tcl8.6,
lib/tcl9.0)
tcl<M>.<m>/ runtimes whose archive mounts at the
executable's own path rather than at
//zipfs:/app, so [info library] is
<exe>/tcl8.6 - the androwish/undroidwish
zipfs backport for Tcl 8.6 does this
A candidate directory qualifies only when it holds init.tcl AND at
least one companion file a real Tcl library carries beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or
an encoding/ subdirectory. Without that second test a package's own
init script - lib/BWidget1.10.1/init.tcl, present in every punkshell
kit - would answer for a tcl library it cannot provide.
Returned dict keys:
ok 1 when at least one qualifying library was found
locations qualifying directories, relative to vfsfolder
rejected candidate directories that held init.tcl but no
companion, relative to vfsfolder - the near-misses
worth naming when a gate fires unexpectedly
checked the location patterns examined, relative to vfsfolder
reason why ok is 0; empty string when ok is 1"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to inspect"
}]
}
proc vfs_boot_library_report {vfsfolder} {
variable boot_library_companions
set checked [list tcl_library lib/tcl<M>.<m> tcl<M>.<m>]
set report [dict create ok 0 locations {} rejected {} checked $checked reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report reason "no such directory: $vfsfolder"
return $report
}
#candidates are the three conventions only - a full tree walk would find every
#package's init.tcl and cost more than the check is worth.
#The tcl[0-9]* globs deliberately also match module trees named tcl8/ or tcl9/
#(both exist in this project's kits); they carry no init.tcl, so the qualification
#test below rejects them without needing a narrower pattern.
set candidates [list [file join $vfsfolder tcl_library]]
foreach dir [lsort [glob -nocomplain -type d -directory [file join $vfsfolder lib] -- {tcl[0-9]*}]] {
lappend candidates $dir
}
foreach dir [lsort [glob -nocomplain -type d -directory $vfsfolder -- {tcl[0-9]*}]] {
lappend candidates $dir
}
set locations [list]
set rejected [list]
foreach dir $candidates {
if {![file isfile [file join $dir init.tcl]]} {
continue
}
set qualifies 0
foreach companion $boot_library_companions {
if {[file isfile [file join $dir $companion]]} {
set qualifies 1
break
}
}
if {!$qualifies && [file isdirectory [file join $dir encoding]]} {
set qualifies 1
}
set rel [string range $dir [expr {[string length $vfsfolder] + 1}] end]
if {$qualifies} {
lappend locations $rel
} else {
lappend rejected $rel
}
}
if {[llength $locations]} {
dict set report ok 1
dict set report locations $locations
dict set report rejected $rejected
return $report
}
dict set report rejected $rejected
if {[llength $rejected]} {
dict set report reason "no tcl library in [file tail $vfsfolder]: [join $rejected {, }] hold init.tcl but none of the companion files a tcl library carries beside it ([join $boot_library_companions {, }]) nor an encoding/ directory"
} else {
dict set report reason "no tcl library in [file tail $vfsfolder]: none of tcl_library/init.tcl, lib/tcl<major>.<minor>/init.tcl or tcl<major>.<minor>/init.tcl is present"
}
return $report
}
#G-133 binary-arch scan support. Recognition data is deliberately plain namespace
#variables so extending it (a new vendor spelling, a new cpu token) is a visible
#one-line edit rather than a code change.
#
#PE Machine field values (IMAGE_FILE_HEADER.Machine, 2 bytes LE at e_lfanew+4)
variable pe_machine_map {
014c i386
8664 x86_64
aa64 arm64
01c0 arm
01c4 arm
0200 ia64
0166 mips
01f0 ppc
5032 riscv32
5064 riscv64
}
#ELF e_machine values (2 bytes at offset 18, endianness per EI_DATA)
variable elf_machine_map {
3 i386
62 x86_64
183 arm64
40 arm
8 mips
20 ppc
21 ppc64
22 s390x
2 sparc
43 sparc64
243 riscv
}
#Mach-O cputype base values (CPU_ARCH_ABI64 flag 0x01000000 selects the 64-bit variant)
variable macho_cputype_map {
7 {i386 x86_64}
12 {arm arm64}
18 {ppc ppc64}
}
#os / cpu tokens accepted in a CANONICAL platform-dir segment (<os>-<cpu>). The os and
#cpu lists follow the punk::platform canon plus common alias tokens, so a platform-
#discriminated subdir is recognised whichever spelling a package author normalised to.
variable platform_segment_os {
win32 windows linux macosx freebsd netbsd openbsd dragonfly solaris sunos
aix hpux irix qnx haiku android msys cygwin
}
variable platform_segment_cpu {
x86_64 amd64 x64 ix86 i386 i486 i586 i686 x86
arm64 aarch64 arm armv6 armv7 armv6l armv7l
ppc ppc64 ppc64le powerpc mips mipsel mips64
riscv32 riscv64 s390 s390x sparc sparc64 universal
}
#Recognised VENDOR spellings observed (or near-certain) as platform-discriminating
#directory names that do not parse as <canonical os>-<canonical cpu>. blend2d,
#extrafont and tclMuPDF ship win-x64 today.
variable platform_segment_vendor {
win-x64 win-x86 win-arm64 win-arm win32 win64
linux-x64 linux-arm64 macos-x64 macos-arm64 osx-x64 osx-arm64
}
#File tails treated as binary libraries by vfs_binary_arch_report
variable binary_library_globs {*.dll *.so *.so.[0-9]* *.dylib}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::binary_arch_classify
@cmd -name "::punkboot::utils::binary_arch_classify"\
-summary\
"Classify a binary library file's executable format and cpu architecture"\
-help\
"Reads only the file's header bytes (a couple of small reads - nothing
is executed or loaded) and classifies the executable format and cpu
architecture:
PE Machine field (2 bytes at e_lfanew+4): i386, x86_64, arm64...
ELF e_machine honouring EI_DATA endianness
Mach-O thin (cputype) and fat/universal (per-member cputypes)
Unrecognised content is reported honestly as format 'unknown' rather
than guessed.
Returned dict keys:
format pe | elf | macho | unknown
arch i386 | x86_64 | arm64 | ... | universal | unknown |
unknown-0x<hex> (a real header whose machine value is not
in the recognition map - definite, just unmapped)
archs member architectures (macho universal only; absent otherwise)
detail short human-readable classification"
@leaders
libfile -type string -optional 0 -help\
"Path of the binary library file to classify"
}]
}
proc binary_arch_classify {libfile} {
variable pe_machine_map
variable elf_machine_map
variable macho_cputype_map
if {[catch {
set fd [open $libfile r]
chan configure $fd -translation binary
set head [read $fd 64]
} errM]} {
catch {close $fd}
return [dict create format unknown arch unknown detail "unreadable: $errM"]
}
set result [dict create format unknown arch unknown detail "not a recognised binary library format"]
try {
if {[string length $head] < 8} {
dict set result detail "too short to carry an executable header ([string length $head] bytes)"
return $result
}
set magic4 [string range $head 0 3]
if {[string range $head 0 1] eq "MZ"} {
#PE: e_lfanew at 0x3c points at "PE\0\0" + IMAGE_FILE_HEADER
if {[binary scan $head @60iu e_lfanew] != 1} {
dict set result detail "MZ header too short for e_lfanew"
return $result
}
seek $fd $e_lfanew
set pehdr [read $fd 6]
if {[string length $pehdr] < 6 || [string range $pehdr 0 3] ne "PE\x00\x00"} {
dict set result detail "MZ header without PE signature (DOS executable?)"
return $result
}
binary scan $pehdr @4su machine
set hexmachine [format %04x $machine]
if {[dict exists $pe_machine_map $hexmachine]} {
set arch [dict get $pe_machine_map $hexmachine]
set result [dict create format pe arch $arch detail "PE $arch"]
} else {
set result [dict create format pe arch unknown-0x$hexmachine detail "PE machine 0x$hexmachine (not in recognition map)"]
}
} elseif {$magic4 eq "\x7fELF"} {
binary scan $head @4cu elfclass
binary scan $head @5cu elfdata
if {$elfdata == 2} {
binary scan $head @18Su e_machine ;#big-endian
} else {
binary scan $head @18su e_machine ;#little-endian (and honest-enough default)
}
if {[dict exists $elf_machine_map $e_machine]} {
set arch [dict get $elf_machine_map $e_machine]
if {$arch eq "riscv"} {
set arch [expr {$elfclass == 2 ? "riscv64" : "riscv32"}]
}
set result [dict create format elf arch $arch detail "ELF $arch"]
} else {
set result [dict create format elf arch unknown-$e_machine detail "ELF e_machine $e_machine (not in recognition map)"]
}
} else {
binary scan $head Iu be32
switch -- [format %08x $be32] {
feedface - feedfacf - cefaedfe - cffaedfe {
#thin Mach-O; cputype follows the magic in the file's own byte order
if {[string index $head 0] eq "\xfe"} {
binary scan $head @4Iu cputype ;#big-endian file
} else {
binary scan $head @4iu cputype
}
set is64 [expr {($cputype & 0x01000000) != 0}]
set base [expr {$cputype & 0x00ffffff}]
if {[dict exists $macho_cputype_map $base]} {
set arch [lindex [dict get $macho_cputype_map $base] $is64]
set result [dict create format macho arch $arch detail "Mach-O $arch"]
} else {
set result [dict create format macho arch unknown-cputype-$base detail "Mach-O cputype $base (not in recognition map)"]
}
}
cafebabe - cafebabf {
#fat/universal (big-endian header). A Java class file shares the
#magic - its version field where nfat_arch sits is >= 45, far above
#any credible member count, so an implausible count stays unknown.
binary scan $head @4Iu nfat
if {$nfat == 0 || $nfat > 30} {
dict set result detail "cafebabe magic with implausible member count $nfat (java class file?)"
return $result
}
set entrysize [expr {$be32 == 0xcafebabf ? 32 : 20}]
seek $fd 8
set fatdata [read $fd [expr {$nfat * $entrysize}]]
set archs [list]
for {set i 0} {$i < $nfat} {incr i} {
if {[binary scan $fatdata @[expr {$i * $entrysize}]Iu cputype] != 1} {
break
}
set is64 [expr {($cputype & 0x01000000) != 0}]
set base [expr {$cputype & 0x00ffffff}]
if {[dict exists $macho_cputype_map $base]} {
lappend archs [lindex [dict get $macho_cputype_map $base] $is64]
} else {
lappend archs unknown-cputype-$base
}
}
set result [dict create format macho arch universal archs $archs detail "Mach-O universal ([join $archs {, }])"]
}
}
}
return $result
} finally {
catch {close $fd}
}
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::platform_expected_binary
@cmd -name "::punkboot::utils::platform_expected_binary"\
-summary\
"Expected binary format/arch for a canonical target platform name"\
-help\
"Maps a canonical punkshell platform name (<os>-<cpu>, e.g
win32-ix86) to the executable format and classifier arch token a
native binary library for that platform carries. Returns a dict
{format F arch A}, or an empty dict when the os or cpu token has
no recognised mapping (callers treat that as scan-not-applicable
rather than guessing)."
@leaders
targetplatform -type string -optional 0 -help\
"Canonical platform name, e.g win32-x86_64, linux-x86_64, macosx-arm64"
}]
}
proc platform_expected_binary {targetplatform} {
set idx [string first - $targetplatform]
if {$idx <= 0} {
return [dict create]
}
set os [string range $targetplatform 0 [expr {$idx - 1}]]
set cpu [string range $targetplatform [expr {$idx + 1}] end]
switch -- $os {
win32 - windows {set format pe}
linux - freebsd - netbsd - openbsd - dragonfly - solaris - sunos {set format elf}
macosx - darwin - macos {set format macho}
default {return [dict create]}
}
switch -- $cpu {
ix86 - i386 - i486 - i586 - i686 - x86 {set arch i386}
x86_64 - amd64 - x64 {set arch x86_64}
arm64 - aarch64 {set arch arm64}
arm - armv6 - armv7 {set arch arm}
ppc - ppc64 - riscv64 - mips - s390x - sparc - sparc64 {set arch $cpu}
default {return [dict create]}
}
return [dict create format $format arch $arch]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::platform_discriminated_segment
@cmd -name "::punkboot::utils::platform_discriminated_segment"\
-summary\
"Whether a directory name is a recognised platform-discriminating segment"\
-help\
"True when the segment is a canonical <os>-<cpu> platform-dir name
(os and cpu each from the recognised token lists), the bare
universal 'macosx' folder, or one of the recognised vendor
spellings (blend2d-style win-x64 etc). Binary libraries under such
a directory are selected per-platform by their package's own index
logic, so a multi-arch payload is legitimate there."
@leaders
segment -type string -optional 0 -help\
"One directory name (a single path segment, not a path)"
}]
}
proc platform_discriminated_segment {segment} {
variable platform_segment_os
variable platform_segment_cpu
variable platform_segment_vendor
set segment [string tolower $segment]
if {$segment eq "macosx"} {
return 1 ;#the universal store-dir convention
}
if {$segment in $platform_segment_vendor} {
return 1
}
set idx [string first - $segment]
if {$idx > 0} {
set os [string range $segment 0 [expr {$idx - 1}]]
set cpu [string range $segment [expr {$idx + 1}] end]
if {$os in $platform_segment_os && $cpu in $platform_segment_cpu} {
return 1
}
}
return 0
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_binary_arch_report
@cmd -name "::punkboot::utils::vfs_binary_arch_report"\
-summary\
"Advisory scan of an assembled vfs tree for binary libraries that mismatch the kit's target"\
-help\
"Walks a merged .vfs tree, classifies each binary library file
(*.dll, *.so, *.so.<n>, *.dylib) by header (binary_arch_classify)
and compares it against the kit's declared target platform. A
library under a platform-discriminated subdirectory (canonical
<os>-<cpu> names, the universal macosx folder, or a recognised
vendor spelling such as win-x64) is EXEMPT - multi-arch payloads
are legitimate when a platform segment lets the package's own
index logic pick per platform. Purely structural: header bytes are
read, nothing is executed or loaded, so the scan also covers
cross-target kits the build host could never run.
A mismatch is reported only when the classification is DEFINITE
(a real PE/ELF/Mach-O header whose format or architecture differs
from the target's); unclassifiable files are counted as unknown,
never warned about. What this cannot see: statically linked
packages, pure-tcl packages whose binary dependency is elsewhere,
and package resolution order (a wrong-arch copy shadowing a
working one by version) - only an in-artifact 'package require'
smoke probe observes those.
Returned dict keys:
ok 1 when no mismatches were found (advisory)
target the target platform as supplied
expected {format F arch A} for the target ({} if unmapped)
scanned binary library files classified
matched files matching the expected format/arch
exempt files under platform-discriminated subdirs (not read)
unknown files whose classification was indefinite
mismatches list of {file <rel> format F arch A detail D}
reason why the scan did not apply; empty when it ran"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to scan"
targetplatform -type string -optional 0 -help\
"Canonical target platform of the kit, e.g win32-ix86"
}]
}
proc vfs_binary_arch_report {vfsfolder targetplatform} {
variable binary_library_globs
set report [dict create ok 1 target $targetplatform expected {} scanned 0 matched 0 exempt 0 unknown 0 mismatches {} reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report ok 0
dict set report reason "no such directory: $vfsfolder"
return $report
}
set expected [platform_expected_binary $targetplatform]
dict set report expected $expected
if {![dict size $expected]} {
dict set report reason "target platform '$targetplatform' has no recognised binary format/arch mapping - scan not applicable"
return $report
}
set expected_format [dict get $expected format]
set expected_arch [dict get $expected arch]
set scanned 0
set matched 0
set exempt 0
set unknown 0
set mismatches [list]
set rootlen [string length $vfsfolder]
#iterative walk - plain Tcl 8.6, no deps. Exemption is decided per DIRECTORY
#(one platform_discriminated_segment test as each dir is queued), so files
#under an exempt subtree are counted without being read.
set pending [list [list $vfsfolder 0]]
while {[llength $pending]} {
set entry [lindex $pending end]
set pending [lrange $pending 0 end-1]
lassign $entry dir dir_exempt
foreach sub [glob -nocomplain -types d -directory $dir *] {
set sub_exempt $dir_exempt
if {!$sub_exempt && [platform_discriminated_segment [file tail $sub]]} {
set sub_exempt 1
}
lappend pending [list $sub $sub_exempt]
}
foreach libfile [glob -nocomplain -types f -directory $dir -- {*}$binary_library_globs] {
if {$dir_exempt} {
incr exempt
continue
}
incr scanned
set class [binary_arch_classify $libfile]
set cformat [dict get $class format]
set carch [dict get $class arch]
if {$cformat eq "unknown"} {
incr unknown
continue
}
set rel [string map {\\ /} [string range $libfile [expr {$rootlen + 1}] end]]
if {$cformat ne $expected_format} {
lappend mismatches [dict create file $rel format $cformat arch $carch detail [dict get $class detail]]
continue
}
if {$carch eq $expected_arch} {
incr matched
continue
}
if {$carch eq "universal" && [dict exists $class archs] && $expected_arch in [dict get $class archs]} {
incr matched
continue
}
#definite finding either way: a differing mapped arch, or a real header
#whose unmapped machine value is provably not the expected architecture
lappend mismatches [dict create file $rel format $cformat arch $carch detail [dict get $class detail]]
}
}
dict set report scanned $scanned
dict set report matched $matched
dict set report exempt $exempt
dict set report unknown $unknown
dict set report mismatches $mismatches
dict set report ok [expr {[llength $mismatches] == 0}]
return $report
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.5.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.5.0
}]
return

229
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -641,9 +641,10 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
# mapfile path as supplied # mapfile path as supplied
# default_target target platform for entries that declare none # default_target target platform for entries that declare none
# runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "") # runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "")
# vfs_runtime_map vfstail -> list of {runtime appname type target} (defaults # vfs_runtime_map vfstail -> list of {runtime appname type target smokerequires}
# applied; only vfs folders that exist on disk - matching the # (defaults applied; only vfs folders that exist on disk - matching
# historical inline parse the kit loop was built around) # the historical inline parse the kit loop was built around;
# smokerequires is the G-133 smoke-require package list, may be "")
# runtime_target runtime -> resolved target platform (all runtimes, mapped order) # runtime_target runtime -> resolved target platform (all runtimes, mapped order)
# missing names of referenced-but-absent runtimes/vfs folders # missing names of referenced-but-absent runtimes/vfs folders
# warnings warning lines in encounter order, ready to print verbatim # warnings warning lines in encounter order, ready to print verbatim
@ -732,8 +733,10 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
} }
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
switch -- [llength $vfsconfig] { switch -- [llength $vfsconfig] {
1 - 2 - 3 - 4 { 1 - 2 - 3 - 4 - 5 {
lassign $vfsconfig vfstail appname target_kit_type #5th element (G-133): smoke-require package list for the built kit -
#interim line format; the schema's eventual home is the G-024 toml conversion
lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} { if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} {
lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime" lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime"
lappend missing $vfstail lappend missing $vfstail
@ -741,11 +744,11 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
if {$appname eq ""} { if {$appname eq ""} {
set appname [file rootname $vfstail] set appname [file rootname $vfstail]
} }
dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target] dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target $smokerequires]
} }
} }
default { default {
set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 4 elements {vfsfolder ?kitname? ?kittype? ?targetplatform?}. got: $vfsconfig ([llength $vfsconfig])" set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 5 elements {vfsfolder ?kitname? ?kittype? ?targetplatform? ?smokerequires?}. got: $vfsconfig ([llength $vfsconfig])"
lappend warnings $badline lappend warnings $badline
lappend badentries $badline lappend badentries $badline
} }
@ -791,6 +794,7 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
# store_tier bin/runtime tier folder name for the target # store_tier bin/runtime tier folder name for the target
# vfs vfs folder tail (e.g punk9wintk903.vfs) # vfs vfs folder tail (e.g punk9wintk903.vfs)
# vfs_present 1|0 # vfs_present 1|0
# smokerequire G-133 smoke-require package list as configured ("" when undeclared)
proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} { proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
set runtime_vfs_map [dict get $model runtime_vfs_map] set runtime_vfs_map [dict get $model runtime_vfs_map]
set vfs_runtime_map [dict get $model vfs_runtime_map] set vfs_runtime_map [dict get $model vfs_runtime_map]
@ -804,7 +808,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
} }
set records [list] set records [list]
set exe_names_seen [list] set exe_names_seen [list]
set record_pair {{rtname vfstail appname target_kit_type} { set record_pair {{rtname vfstail appname target_kit_type smokerequires} {
upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\ upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\
runtime_target runtime_target default_target default_target runtime_target runtime_target default_target default_target
if {$appname eq ""} { if {$appname eq ""} {
@ -850,6 +854,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
store_tier $store_tier\ store_tier $store_tier\
vfs $vfstail\ vfs $vfstail\
vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\ vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\
smokerequire $smokerequires\
] ]
}} }}
foreach vfstail $vfs_tails { foreach vfstail $vfs_tails {
@ -862,25 +867,25 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
continue continue
} }
foreach vfs_app [dict get $runtime_vfs_map $rtname] { foreach vfs_app [dict get $runtime_vfs_map $rtname] {
lassign $vfs_app configured_vfs appname target_kit_type lassign $vfs_app configured_vfs appname target_kit_type - smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
} }
#entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse) #entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse)
dict for {rtname vfs_specs} $runtime_vfs_map { dict for {rtname vfs_specs} $runtime_vfs_map {
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 4} { if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 5} {
continue continue
} }
lassign $vfsconfig vfstail appname target_kit_type lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {[dict exists $vfs_runtime_map $vfstail]} { if {[dict exists $vfs_runtime_map $vfstail]} {
continue ;#already enumerated above continue ;#already enumerated above
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
return $records return $records
@ -2282,6 +2287,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe.vfs src/_build/<kit>.exe.vfs
| |
| advisory payload/target binary-arch scan of the merged
| tree (wrong-arch libraries -> recapped BUILD-WARNING) [K11]
| boot-precondition gate: merged vfs must supply a tcl | boot-precondition gate: merged vfs must supply a tcl
| library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10] | library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10]
v v
@ -2294,6 +2301,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe src/_build/<kit>.exe
| |
| smoke-require probe (host-runnable kits with declared
| packages): plain 'package require' inside the artifact [K11]
| deploy step: delete old bin/<kit>.exe, copy new one in | deploy step: delete old bin/<kit>.exe, copy new one in
v v
bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a
@ -2377,6 +2386,22 @@ KEY / NOTES
own payload could not be extracted - see the BUILD-WARNING naming what was own payload could not be extracted - see the BUILD-WARNING naming what was
tried. 'make.tcl check' reports whether the gate is ACTIVE. tried. 'make.tcl check' reports whether the gate is ACTIVE.
[K11] PAYLOAD/TARGET consistency checks (G-133), both ADVISORY - warnings recap at
end of run, the kit still builds and deploys. (a) Binary-arch scan at the same
seam as [K10]: each binary library (*.dll/*.so/*.dylib) in the merged tree is
classified by header (PE/ELF/Mach-O - structural, nothing executed, so
cross-target kits are covered) against the kit's target platform; wrong-arch
libraries outside platform-discriminated subdirs (win32-ix86/, win-x64/ etc -
multi-arch payloads are legitimate there) earn recapped BUILD-WARNINGs.
(b) Smoke-require probe: a kit declaring packages (mapvfs.config 5th entry
element) has each one plain-'package require'd INSIDE the freshly built
artifact via its tclsh subcommand - the only check that sees resolution-order
defects (a wrong-arch higher-versioned package shadowing a working copy);
cross-target kits skip with a stated reason, undeclared kits run nothing new.
What neither check guarantees: statically linked packages, pure-tcl packages
with binary dependencies, and version-preference outcomes are visible only to
the smoke probe - and only for declared packages. 'make.tcl check' reports both.
MAINTENANCE (agents take note) MAINTENANCE (agents take note)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
This text is embedded in make.tcl (::punkboot::workflow_text). When the build This text is embedded in make.tcl (::punkboot::workflow_text). When the build
@ -3356,6 +3381,95 @@ proc ::punkboot::get_vfs_boot_library_report {vfsfolder} {
} }
return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]] return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]]
} }
#Availability probe + advisory payload/target binary-arch scan of an assembled kit vfs
#(G-133), shared by the bake loop and the 'check' command report. Same guarded-require
#treatment as the boot-precondition check above: a stale bootsupport snapshot degrades
#the scan to a NOTE. Returns {available report}.
proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vfs_binary_arch_report]]\
}]
if {!$available} {
return [list 0 [dict create]]
}
return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]]
}
#G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh'
#subcommand and plain-'package require' each declared package inside it. This is the
#only check that observes actual package RESOLUTION in the artifact - the punkluck86
#incident class (a higher-versioned wrong-arch Thread shadowing the runtime's working
#copy on plain require) is invisible to every structural check because the working
#file exists. The tclsh subcommand is used deliberately: it exits cleanly even on
#runtimes whose full repl teardown is fragile, and a script argument with drained
#stdin cannot reach any console-reopen path. ADVISORY: failures become recapped
#BUILD-WARNINGs naming kit, package and the actual error - the kit still deploys.
#Returns 1 when every declared package resolved, else 0.
proc ::punkboot::kit_smoke_require_probe {kitpath targetkit packages} {
set probescript [file rootname $kitpath]_smokerequire.tcl
if {[catch {
set fd [open $probescript w]
chan configure $fd -translation lf
puts $fd "#generated by make.tcl bake (G-133) - smoke-require probe for $targetkit"
puts $fd "set packages [list $packages]"
puts $fd {foreach pkg $packages {
if {[catch {package require $pkg} v]} {
puts stdout [list SMOKE-REQUIRE $pkg FAIL $v]
} else {
puts stdout [list SMOKE-REQUIRE $pkg OK $v]
}
}
exit 0}
close $fd
} errM]} {
catch {close $fd}
::punkboot::print_build_warnings [list "smoke-require for kit $targetkit could not write its probe script ($probescript): $errM"]
return 0
}
if {[catch {
lassign [punk::lib::invoke [list [file nativename $kitpath] tclsh [file nativename $probescript] << ""]] p_stdout p_stderr p_exitcode
} errM]} {
::punkboot::print_build_warnings [list "smoke-require FAILED for kit $targetkit: could not execute freshly built artifact via its tclsh subcommand ($errM)"]
return 0
}
set seen [dict create]
foreach line [split $p_stdout \n] {
set line [string trim $line]
if {![string match "SMOKE-REQUIRE *" $line]} {
continue
}
#each line is a well-formed tcl list: SMOKE-REQUIRE <pkg> OK|FAIL <version|error>
if {[catch {lassign $line - r_pkg r_status r_detail}]} {
continue
}
dict set seen $r_pkg [list $r_status $r_detail]
}
set allok 1
set warnings [list]
foreach pkg $packages {
if {![dict exists $seen $pkg]} {
set allok 0
set tail [string trim [join [lrange [split [string trim $p_stderr] \n] end-2 end] " | "]]
lappend warnings "smoke-require FAILED for kit $targetkit: no result for package '$pkg' (artifact exited $p_exitcode before reporting[expr {$tail ne "" ? "; stderr tail: $tail" : ""}])"
continue
}
lassign [dict get $seen $pkg] r_status r_detail
if {$r_status eq "OK"} {
puts stdout " smoke-require OK for kit $targetkit: package require $pkg -> $r_detail"
} else {
set allok 0
lappend warnings "smoke-require FAILED for kit $targetkit: package require $pkg -> $r_detail"
}
}
if {$p_exitcode != 0} {
set allok 0
lappend warnings "smoke-require for kit $targetkit: artifact exited with code $p_exitcode via its tclsh subcommand (expected 0)"
}
if {[llength $warnings]} {
::punkboot::print_build_warnings $warnings
}
return $allok
}
#Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter #Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter
#which of make.tcl's many exit points a command takes. #which of make.tcl's many exit points a command takes.
if {![llength [info commands ::punkboot::exit_original]]} { if {![llength [info commands ::punkboot::exit_original]]} {
@ -3612,6 +3726,40 @@ if {$::punkboot::command eq "check"} {
puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)" puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'." puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'."
} }
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
lassign [::punkboot::get_vfs_binary_arch_report [file join $projectroot __punkboot_archscan_probe__] $::punkboot::target_platform] _arch_available _arch_report
if {$_arch_available} {
puts stdout "payload/target arch scan (G-133): ACTIVE (advisory)"
puts stdout " bake classifies each merged kit's binary libraries (*.dll/*.so/*.dylib header bytes - structural,"
puts stdout " nothing executed, cross-target kits included) against the kit's target platform and emits"
puts stdout " recapped BUILD-WARNINGs for wrong-arch libraries outside platform-discriminated subdirs"
puts stdout " (canonical <os>-<cpu> names and recognised vendor spellings such as win-x64 are exempt)."
} else {
puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'."
}
set _smoke_declared [list]
if {[file exists $sourcefolder/runtime/mapvfs.config]} {
set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform]
foreach _rec [punkboot::lib::mapvfs_kit_outputs $_smokemodel [file dirname $sourcefolder]/bin/runtime $sourcefolder] {
if {[llength [dict get $_rec smokerequire]]} {
lappend _smoke_declared "[dict get $_rec kitname] ([join [dict get $_rec smokerequire] {, }])"
}
}
unset -nocomplain _smokemodel _rec
}
puts stdout "smoke-require probe (G-133, advisory): freshly built host-runnable kits declaring packages"
puts stdout " (mapvfs.config 5th entry element) have each one plain-'package require'd inside the built"
puts stdout " artifact via its tclsh subcommand - the only check that sees resolution-order defects such as"
puts stdout " a wrong-arch higher-versioned package shadowing a working copy. Failures are recapped"
puts stdout " BUILD-WARNINGs; cross-target kits skip with a stated reason; undeclared kits run nothing new."
if {[llength $_smoke_declared]} {
puts stdout " declared: [join $_smoke_declared {; }]"
} else {
puts stdout " declared: (none)"
}
puts stdout $sep puts stdout $sep
exit 0 exit 0
} }
@ -5840,7 +5988,8 @@ foreach vfstail $vfs_tails {
if {[dict exists $runtime_vfs_map $rtname]} { if {[dict exists $runtime_vfs_map $rtname]} {
set applist [dict get $runtime_vfs_map $rtname] set applist [dict get $runtime_vfs_map $rtname]
foreach vfs_app $applist { foreach vfs_app $applist {
lassign $vfs_app configured_vfs appname target_kit_type #5th element = G-133 smoke-require package list ("" when undeclared)
lassign $vfs_app configured_vfs appname target_kit_type - kit_smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
@ -5862,7 +6011,7 @@ foreach vfstail $vfs_tails {
} }
} }
lappend exe_names_seen $targetkit lappend exe_names_seen $targetkit
lappend targetkits [list $targetkit $target_kit_type] lappend targetkits [list $targetkit $target_kit_type $kit_smokerequires]
} }
} }
puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits" puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits"
@ -5871,7 +6020,7 @@ foreach vfstail $vfs_tails {
continue ;#G-121 selective bake - not a requested kit output continue ;#G-121 selective bake - not a requested kit output
} }
puts stdout " processing targetkit: $targetkit_info" puts stdout " processing targetkit: $targetkit_info"
lassign $targetkit_info targetkit target_kit_type lassign $targetkit_info targetkit target_kit_type kit_smokerequires
#Self-build guard: never process the kit whose deployed executable is running this #Self-build guard: never process the kit whose deployed executable is running this
#build - the process sweep below would kill this very process, and the deploy step #build - the process sweep below would kill this very process, and the deploy step
#cannot replace a running image anyway. Informational/update subcommands are #cannot replace a running image anyway. Informational/update subcommands are
@ -6240,6 +6389,37 @@ foreach vfstail $vfs_tails {
set sourcevfs [file join $sourcefolder vfs $vfstail] set sourcevfs [file join $sourcefolder vfs $vfstail]
merge_over $sourcevfs $targetvfs merge_over $sourcevfs $targetvfs
#G-133 advisory payload/target binary-arch scan, at the same post-merge
#seam as the G-125 gate below. Structural (header bytes only, nothing
#executed), so it runs for every kit including cross-target ones. A
#binary library under a platform-discriminated subdir (win32-ix86/,
#win-x64/ etc) is exempt - multi-arch payloads are legitimate there.
#ADVISORY by design: vendor layouts make false certainty unavoidable,
#so mismatches warn (recapped) and the kit still builds - the G-125
#gate below stays the only refusal at this seam.
lassign [::punkboot::get_vfs_binary_arch_report $targetvfs $rt_target] archscan_available archscan
if {!$archscan_available} {
puts stderr "NOTE: kit payload binary-arch scan unavailable (punkboot::utils vfs_binary_arch_report not loadable from bootsupport) - continuing without it"
} elseif {[dict get $archscan reason] ne ""} {
puts stdout " payload binary-arch scan not applicable for kit $targetkit: [dict get $archscan reason]"
} elseif {[llength [dict get $archscan mismatches]]} {
set archscan_mismatches [dict get $archscan mismatches]
set archwarnings [list]
set shown 0
foreach m $archscan_mismatches {
if {$shown >= 8} {break}
incr shown
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [dict get $m file] is [dict get $m detail] outside any platform-discriminated subdir - a $rt_target runtime cannot load it"
}
if {[llength $archscan_mismatches] > $shown} {
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [expr {[llength $archscan_mismatches] - $shown}] further mismatching binary libraries not listed above ([llength $archscan_mismatches] mismatches total, [dict get $archscan scanned] binary libraries scanned)"
}
::punkboot::print_build_warnings $archwarnings
unset archwarnings archscan_mismatches shown
} else {
puts stdout " payload binary-arch scan for kit $targetkit (target $rt_target): OK - [dict get $archscan scanned] binary libraries scanned ([dict get $archscan matched] match target, [dict get $archscan exempt] exempt under platform subdirs, [dict get $archscan unknown] unclassifiable)"
}
#G-125 boot-precondition gate. A kit with no tcl library cannot initialise #G-125 boot-precondition gate. A kit with no tcl library cannot initialise
#at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any #at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any
#build product is written, and long before the deploy step deletes the #build product is written, and long before the deploy step deletes the
@ -6588,6 +6768,23 @@ foreach vfstail $vfs_tails {
# -- --- --- --- --- --- # -- --- --- --- --- ---
$vfs_event targetset_end OK $vfs_event targetset_end OK
#G-133 post-build smoke-require probe. Only an actual 'package require'
#inside the built artifact observes package RESOLUTION - the wrong-arch
#version-shadowing class is invisible to every structural check because
#the working file exists too. Runs only for kits that declare
#smoke-require packages (mapvfs.config 5th entry element) and only when
#this host can execute the artifact; undeclared kits run nothing new.
if {[llength $kit_smokerequires]} {
if {$rtname eq "-"} {
puts stdout " smoke-require skipped for kit $targetkit: a runtimeless .kit artifact is not directly executable"
} elseif {[punkboot::lib::platform_process_family $rt_target] ne [punkboot::lib::platform_process_family $::punkboot::host_platform]} {
puts stdout " smoke-require skipped for kit $targetkit: cross-target ($rt_target) - the artifact is not executable on this $::punkboot::host_platform host"
} else {
puts stdout " smoke-require probe for kit $targetkit (packages: $kit_smokerequires) - executing freshly built artifact via its tclsh subcommand"
::punkboot::kit_smoke_require_probe $buildfolder/$targetkit $targetkit $kit_smokerequires
}
}
} else { } else {
set skipped_vfs_build 1 set skipped_vfs_build 1
puts stderr "." puts stderr "."

229
src/project_layouts/vendor/punk/basic/src/make.tcl vendored

@ -641,9 +641,10 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
# mapfile path as supplied # mapfile path as supplied
# default_target target platform for entries that declare none # default_target target platform for entries that declare none
# runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "") # runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "")
# vfs_runtime_map vfstail -> list of {runtime appname type target} (defaults # vfs_runtime_map vfstail -> list of {runtime appname type target smokerequires}
# applied; only vfs folders that exist on disk - matching the # (defaults applied; only vfs folders that exist on disk - matching
# historical inline parse the kit loop was built around) # the historical inline parse the kit loop was built around;
# smokerequires is the G-133 smoke-require package list, may be "")
# runtime_target runtime -> resolved target platform (all runtimes, mapped order) # runtime_target runtime -> resolved target platform (all runtimes, mapped order)
# missing names of referenced-but-absent runtimes/vfs folders # missing names of referenced-but-absent runtimes/vfs folders
# warnings warning lines in encounter order, ready to print verbatim # warnings warning lines in encounter order, ready to print verbatim
@ -732,8 +733,10 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
} }
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
switch -- [llength $vfsconfig] { switch -- [llength $vfsconfig] {
1 - 2 - 3 - 4 { 1 - 2 - 3 - 4 - 5 {
lassign $vfsconfig vfstail appname target_kit_type #5th element (G-133): smoke-require package list for the built kit -
#interim line format; the schema's eventual home is the G-024 toml conversion
lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} { if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} {
lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime" lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime"
lappend missing $vfstail lappend missing $vfstail
@ -741,11 +744,11 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
if {$appname eq ""} { if {$appname eq ""} {
set appname [file rootname $vfstail] set appname [file rootname $vfstail]
} }
dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target] dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target $smokerequires]
} }
} }
default { default {
set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 4 elements {vfsfolder ?kitname? ?kittype? ?targetplatform?}. got: $vfsconfig ([llength $vfsconfig])" set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 5 elements {vfsfolder ?kitname? ?kittype? ?targetplatform? ?smokerequires?}. got: $vfsconfig ([llength $vfsconfig])"
lappend warnings $badline lappend warnings $badline
lappend badentries $badline lappend badentries $badline
} }
@ -791,6 +794,7 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
# store_tier bin/runtime tier folder name for the target # store_tier bin/runtime tier folder name for the target
# vfs vfs folder tail (e.g punk9wintk903.vfs) # vfs vfs folder tail (e.g punk9wintk903.vfs)
# vfs_present 1|0 # vfs_present 1|0
# smokerequire G-133 smoke-require package list as configured ("" when undeclared)
proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} { proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
set runtime_vfs_map [dict get $model runtime_vfs_map] set runtime_vfs_map [dict get $model runtime_vfs_map]
set vfs_runtime_map [dict get $model vfs_runtime_map] set vfs_runtime_map [dict get $model vfs_runtime_map]
@ -804,7 +808,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
} }
set records [list] set records [list]
set exe_names_seen [list] set exe_names_seen [list]
set record_pair {{rtname vfstail appname target_kit_type} { set record_pair {{rtname vfstail appname target_kit_type smokerequires} {
upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\ upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\
runtime_target runtime_target default_target default_target runtime_target runtime_target default_target default_target
if {$appname eq ""} { if {$appname eq ""} {
@ -850,6 +854,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
store_tier $store_tier\ store_tier $store_tier\
vfs $vfstail\ vfs $vfstail\
vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\ vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\
smokerequire $smokerequires\
] ]
}} }}
foreach vfstail $vfs_tails { foreach vfstail $vfs_tails {
@ -862,25 +867,25 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
continue continue
} }
foreach vfs_app [dict get $runtime_vfs_map $rtname] { foreach vfs_app [dict get $runtime_vfs_map $rtname] {
lassign $vfs_app configured_vfs appname target_kit_type lassign $vfs_app configured_vfs appname target_kit_type - smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
} }
#entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse) #entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse)
dict for {rtname vfs_specs} $runtime_vfs_map { dict for {rtname vfs_specs} $runtime_vfs_map {
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 4} { if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 5} {
continue continue
} }
lassign $vfsconfig vfstail appname target_kit_type lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {[dict exists $vfs_runtime_map $vfstail]} { if {[dict exists $vfs_runtime_map $vfstail]} {
continue ;#already enumerated above continue ;#already enumerated above
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
return $records return $records
@ -2282,6 +2287,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe.vfs src/_build/<kit>.exe.vfs
| |
| advisory payload/target binary-arch scan of the merged
| tree (wrong-arch libraries -> recapped BUILD-WARNING) [K11]
| boot-precondition gate: merged vfs must supply a tcl | boot-precondition gate: merged vfs must supply a tcl
| library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10] | library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10]
v v
@ -2294,6 +2301,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe src/_build/<kit>.exe
| |
| smoke-require probe (host-runnable kits with declared
| packages): plain 'package require' inside the artifact [K11]
| deploy step: delete old bin/<kit>.exe, copy new one in | deploy step: delete old bin/<kit>.exe, copy new one in
v v
bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a
@ -2377,6 +2386,22 @@ KEY / NOTES
own payload could not be extracted - see the BUILD-WARNING naming what was own payload could not be extracted - see the BUILD-WARNING naming what was
tried. 'make.tcl check' reports whether the gate is ACTIVE. tried. 'make.tcl check' reports whether the gate is ACTIVE.
[K11] PAYLOAD/TARGET consistency checks (G-133), both ADVISORY - warnings recap at
end of run, the kit still builds and deploys. (a) Binary-arch scan at the same
seam as [K10]: each binary library (*.dll/*.so/*.dylib) in the merged tree is
classified by header (PE/ELF/Mach-O - structural, nothing executed, so
cross-target kits are covered) against the kit's target platform; wrong-arch
libraries outside platform-discriminated subdirs (win32-ix86/, win-x64/ etc -
multi-arch payloads are legitimate there) earn recapped BUILD-WARNINGs.
(b) Smoke-require probe: a kit declaring packages (mapvfs.config 5th entry
element) has each one plain-'package require'd INSIDE the freshly built
artifact via its tclsh subcommand - the only check that sees resolution-order
defects (a wrong-arch higher-versioned package shadowing a working copy);
cross-target kits skip with a stated reason, undeclared kits run nothing new.
What neither check guarantees: statically linked packages, pure-tcl packages
with binary dependencies, and version-preference outcomes are visible only to
the smoke probe - and only for declared packages. 'make.tcl check' reports both.
MAINTENANCE (agents take note) MAINTENANCE (agents take note)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
This text is embedded in make.tcl (::punkboot::workflow_text). When the build This text is embedded in make.tcl (::punkboot::workflow_text). When the build
@ -3356,6 +3381,95 @@ proc ::punkboot::get_vfs_boot_library_report {vfsfolder} {
} }
return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]] return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]]
} }
#Availability probe + advisory payload/target binary-arch scan of an assembled kit vfs
#(G-133), shared by the bake loop and the 'check' command report. Same guarded-require
#treatment as the boot-precondition check above: a stale bootsupport snapshot degrades
#the scan to a NOTE. Returns {available report}.
proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vfs_binary_arch_report]]\
}]
if {!$available} {
return [list 0 [dict create]]
}
return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]]
}
#G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh'
#subcommand and plain-'package require' each declared package inside it. This is the
#only check that observes actual package RESOLUTION in the artifact - the punkluck86
#incident class (a higher-versioned wrong-arch Thread shadowing the runtime's working
#copy on plain require) is invisible to every structural check because the working
#file exists. The tclsh subcommand is used deliberately: it exits cleanly even on
#runtimes whose full repl teardown is fragile, and a script argument with drained
#stdin cannot reach any console-reopen path. ADVISORY: failures become recapped
#BUILD-WARNINGs naming kit, package and the actual error - the kit still deploys.
#Returns 1 when every declared package resolved, else 0.
proc ::punkboot::kit_smoke_require_probe {kitpath targetkit packages} {
set probescript [file rootname $kitpath]_smokerequire.tcl
if {[catch {
set fd [open $probescript w]
chan configure $fd -translation lf
puts $fd "#generated by make.tcl bake (G-133) - smoke-require probe for $targetkit"
puts $fd "set packages [list $packages]"
puts $fd {foreach pkg $packages {
if {[catch {package require $pkg} v]} {
puts stdout [list SMOKE-REQUIRE $pkg FAIL $v]
} else {
puts stdout [list SMOKE-REQUIRE $pkg OK $v]
}
}
exit 0}
close $fd
} errM]} {
catch {close $fd}
::punkboot::print_build_warnings [list "smoke-require for kit $targetkit could not write its probe script ($probescript): $errM"]
return 0
}
if {[catch {
lassign [punk::lib::invoke [list [file nativename $kitpath] tclsh [file nativename $probescript] << ""]] p_stdout p_stderr p_exitcode
} errM]} {
::punkboot::print_build_warnings [list "smoke-require FAILED for kit $targetkit: could not execute freshly built artifact via its tclsh subcommand ($errM)"]
return 0
}
set seen [dict create]
foreach line [split $p_stdout \n] {
set line [string trim $line]
if {![string match "SMOKE-REQUIRE *" $line]} {
continue
}
#each line is a well-formed tcl list: SMOKE-REQUIRE <pkg> OK|FAIL <version|error>
if {[catch {lassign $line - r_pkg r_status r_detail}]} {
continue
}
dict set seen $r_pkg [list $r_status $r_detail]
}
set allok 1
set warnings [list]
foreach pkg $packages {
if {![dict exists $seen $pkg]} {
set allok 0
set tail [string trim [join [lrange [split [string trim $p_stderr] \n] end-2 end] " | "]]
lappend warnings "smoke-require FAILED for kit $targetkit: no result for package '$pkg' (artifact exited $p_exitcode before reporting[expr {$tail ne "" ? "; stderr tail: $tail" : ""}])"
continue
}
lassign [dict get $seen $pkg] r_status r_detail
if {$r_status eq "OK"} {
puts stdout " smoke-require OK for kit $targetkit: package require $pkg -> $r_detail"
} else {
set allok 0
lappend warnings "smoke-require FAILED for kit $targetkit: package require $pkg -> $r_detail"
}
}
if {$p_exitcode != 0} {
set allok 0
lappend warnings "smoke-require for kit $targetkit: artifact exited with code $p_exitcode via its tclsh subcommand (expected 0)"
}
if {[llength $warnings]} {
::punkboot::print_build_warnings $warnings
}
return $allok
}
#Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter #Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter
#which of make.tcl's many exit points a command takes. #which of make.tcl's many exit points a command takes.
if {![llength [info commands ::punkboot::exit_original]]} { if {![llength [info commands ::punkboot::exit_original]]} {
@ -3612,6 +3726,40 @@ if {$::punkboot::command eq "check"} {
puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)" puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'." puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'."
} }
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
lassign [::punkboot::get_vfs_binary_arch_report [file join $projectroot __punkboot_archscan_probe__] $::punkboot::target_platform] _arch_available _arch_report
if {$_arch_available} {
puts stdout "payload/target arch scan (G-133): ACTIVE (advisory)"
puts stdout " bake classifies each merged kit's binary libraries (*.dll/*.so/*.dylib header bytes - structural,"
puts stdout " nothing executed, cross-target kits included) against the kit's target platform and emits"
puts stdout " recapped BUILD-WARNINGs for wrong-arch libraries outside platform-discriminated subdirs"
puts stdout " (canonical <os>-<cpu> names and recognised vendor spellings such as win-x64 are exempt)."
} else {
puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'."
}
set _smoke_declared [list]
if {[file exists $sourcefolder/runtime/mapvfs.config]} {
set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform]
foreach _rec [punkboot::lib::mapvfs_kit_outputs $_smokemodel [file dirname $sourcefolder]/bin/runtime $sourcefolder] {
if {[llength [dict get $_rec smokerequire]]} {
lappend _smoke_declared "[dict get $_rec kitname] ([join [dict get $_rec smokerequire] {, }])"
}
}
unset -nocomplain _smokemodel _rec
}
puts stdout "smoke-require probe (G-133, advisory): freshly built host-runnable kits declaring packages"
puts stdout " (mapvfs.config 5th entry element) have each one plain-'package require'd inside the built"
puts stdout " artifact via its tclsh subcommand - the only check that sees resolution-order defects such as"
puts stdout " a wrong-arch higher-versioned package shadowing a working copy. Failures are recapped"
puts stdout " BUILD-WARNINGs; cross-target kits skip with a stated reason; undeclared kits run nothing new."
if {[llength $_smoke_declared]} {
puts stdout " declared: [join $_smoke_declared {; }]"
} else {
puts stdout " declared: (none)"
}
puts stdout $sep puts stdout $sep
exit 0 exit 0
} }
@ -5840,7 +5988,8 @@ foreach vfstail $vfs_tails {
if {[dict exists $runtime_vfs_map $rtname]} { if {[dict exists $runtime_vfs_map $rtname]} {
set applist [dict get $runtime_vfs_map $rtname] set applist [dict get $runtime_vfs_map $rtname]
foreach vfs_app $applist { foreach vfs_app $applist {
lassign $vfs_app configured_vfs appname target_kit_type #5th element = G-133 smoke-require package list ("" when undeclared)
lassign $vfs_app configured_vfs appname target_kit_type - kit_smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
@ -5862,7 +6011,7 @@ foreach vfstail $vfs_tails {
} }
} }
lappend exe_names_seen $targetkit lappend exe_names_seen $targetkit
lappend targetkits [list $targetkit $target_kit_type] lappend targetkits [list $targetkit $target_kit_type $kit_smokerequires]
} }
} }
puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits" puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits"
@ -5871,7 +6020,7 @@ foreach vfstail $vfs_tails {
continue ;#G-121 selective bake - not a requested kit output continue ;#G-121 selective bake - not a requested kit output
} }
puts stdout " processing targetkit: $targetkit_info" puts stdout " processing targetkit: $targetkit_info"
lassign $targetkit_info targetkit target_kit_type lassign $targetkit_info targetkit target_kit_type kit_smokerequires
#Self-build guard: never process the kit whose deployed executable is running this #Self-build guard: never process the kit whose deployed executable is running this
#build - the process sweep below would kill this very process, and the deploy step #build - the process sweep below would kill this very process, and the deploy step
#cannot replace a running image anyway. Informational/update subcommands are #cannot replace a running image anyway. Informational/update subcommands are
@ -6240,6 +6389,37 @@ foreach vfstail $vfs_tails {
set sourcevfs [file join $sourcefolder vfs $vfstail] set sourcevfs [file join $sourcefolder vfs $vfstail]
merge_over $sourcevfs $targetvfs merge_over $sourcevfs $targetvfs
#G-133 advisory payload/target binary-arch scan, at the same post-merge
#seam as the G-125 gate below. Structural (header bytes only, nothing
#executed), so it runs for every kit including cross-target ones. A
#binary library under a platform-discriminated subdir (win32-ix86/,
#win-x64/ etc) is exempt - multi-arch payloads are legitimate there.
#ADVISORY by design: vendor layouts make false certainty unavoidable,
#so mismatches warn (recapped) and the kit still builds - the G-125
#gate below stays the only refusal at this seam.
lassign [::punkboot::get_vfs_binary_arch_report $targetvfs $rt_target] archscan_available archscan
if {!$archscan_available} {
puts stderr "NOTE: kit payload binary-arch scan unavailable (punkboot::utils vfs_binary_arch_report not loadable from bootsupport) - continuing without it"
} elseif {[dict get $archscan reason] ne ""} {
puts stdout " payload binary-arch scan not applicable for kit $targetkit: [dict get $archscan reason]"
} elseif {[llength [dict get $archscan mismatches]]} {
set archscan_mismatches [dict get $archscan mismatches]
set archwarnings [list]
set shown 0
foreach m $archscan_mismatches {
if {$shown >= 8} {break}
incr shown
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [dict get $m file] is [dict get $m detail] outside any platform-discriminated subdir - a $rt_target runtime cannot load it"
}
if {[llength $archscan_mismatches] > $shown} {
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [expr {[llength $archscan_mismatches] - $shown}] further mismatching binary libraries not listed above ([llength $archscan_mismatches] mismatches total, [dict get $archscan scanned] binary libraries scanned)"
}
::punkboot::print_build_warnings $archwarnings
unset archwarnings archscan_mismatches shown
} else {
puts stdout " payload binary-arch scan for kit $targetkit (target $rt_target): OK - [dict get $archscan scanned] binary libraries scanned ([dict get $archscan matched] match target, [dict get $archscan exempt] exempt under platform subdirs, [dict get $archscan unknown] unclassifiable)"
}
#G-125 boot-precondition gate. A kit with no tcl library cannot initialise #G-125 boot-precondition gate. A kit with no tcl library cannot initialise
#at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any #at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any
#build product is written, and long before the deploy step deletes the #build product is written, and long before the deploy step deletes the
@ -6588,6 +6768,23 @@ foreach vfstail $vfs_tails {
# -- --- --- --- --- --- # -- --- --- --- --- ---
$vfs_event targetset_end OK $vfs_event targetset_end OK
#G-133 post-build smoke-require probe. Only an actual 'package require'
#inside the built artifact observes package RESOLUTION - the wrong-arch
#version-shadowing class is invisible to every structural check because
#the working file exists too. Runs only for kits that declare
#smoke-require packages (mapvfs.config 5th entry element) and only when
#this host can execute the artifact; undeclared kits run nothing new.
if {[llength $kit_smokerequires]} {
if {$rtname eq "-"} {
puts stdout " smoke-require skipped for kit $targetkit: a runtimeless .kit artifact is not directly executable"
} elseif {[punkboot::lib::platform_process_family $rt_target] ne [punkboot::lib::platform_process_family $::punkboot::host_platform]} {
puts stdout " smoke-require skipped for kit $targetkit: cross-target ($rt_target) - the artifact is not executable on this $::punkboot::host_platform host"
} else {
puts stdout " smoke-require probe for kit $targetkit (packages: $kit_smokerequires) - executing freshly built artifact via its tclsh subcommand"
::punkboot::kit_smoke_require_probe $buildfolder/$targetkit $targetkit $kit_smokerequires
}
}
} else { } else {
set skipped_vfs_build 1 set skipped_vfs_build 1
puts stderr "." puts stderr "."

229
src/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -641,9 +641,10 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
# mapfile path as supplied # mapfile path as supplied
# default_target target platform for entries that declare none # default_target target platform for entries that declare none
# runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "") # runtime_vfs_map runtime -> raw vfs_specs as listed (appname/type/target may be "")
# vfs_runtime_map vfstail -> list of {runtime appname type target} (defaults # vfs_runtime_map vfstail -> list of {runtime appname type target smokerequires}
# applied; only vfs folders that exist on disk - matching the # (defaults applied; only vfs folders that exist on disk - matching
# historical inline parse the kit loop was built around) # the historical inline parse the kit loop was built around;
# smokerequires is the G-133 smoke-require package list, may be "")
# runtime_target runtime -> resolved target platform (all runtimes, mapped order) # runtime_target runtime -> resolved target platform (all runtimes, mapped order)
# missing names of referenced-but-absent runtimes/vfs folders # missing names of referenced-but-absent runtimes/vfs folders
# warnings warning lines in encounter order, ready to print verbatim # warnings warning lines in encounter order, ready to print verbatim
@ -732,8 +733,10 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
} }
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
switch -- [llength $vfsconfig] { switch -- [llength $vfsconfig] {
1 - 2 - 3 - 4 { 1 - 2 - 3 - 4 - 5 {
lassign $vfsconfig vfstail appname target_kit_type #5th element (G-133): smoke-require package list for the built kit -
#interim line format; the schema's eventual home is the G-024 toml conversion
lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} { if {![file isdirectory [file join $sourcefolder vfs $vfstail]]} {
lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime" lappend warnings "WARNING: Missing vfs folder [file join $sourcefolder vfs $vfstail] specified in mapvfs.config for runtime $runtime"
lappend missing $vfstail lappend missing $vfstail
@ -741,11 +744,11 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
if {$appname eq ""} { if {$appname eq ""} {
set appname [file rootname $vfstail] set appname [file rootname $vfstail]
} }
dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target] dict lappend vfs_runtime_map $vfstail [list $runtime $appname $target_kit_type $line_target $smokerequires]
} }
} }
default { default {
set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 4 elements {vfsfolder ?kitname? ?kittype? ?targetplatform?}. got: $vfsconfig ([llength $vfsconfig])" set badline "bad entry in mapvfs.config - expected each entry after the runtime name to be a list of 1 to 5 elements {vfsfolder ?kitname? ?kittype? ?targetplatform? ?smokerequires?}. got: $vfsconfig ([llength $vfsconfig])"
lappend warnings $badline lappend warnings $badline
lappend badentries $badline lappend badentries $badline
} }
@ -791,6 +794,7 @@ proc ::punkboot::lib::mapvfs_parse {mapfile rtbase sourcefolder default_target}
# store_tier bin/runtime tier folder name for the target # store_tier bin/runtime tier folder name for the target
# vfs vfs folder tail (e.g punk9wintk903.vfs) # vfs vfs folder tail (e.g punk9wintk903.vfs)
# vfs_present 1|0 # vfs_present 1|0
# smokerequire G-133 smoke-require package list as configured ("" when undeclared)
proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} { proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
set runtime_vfs_map [dict get $model runtime_vfs_map] set runtime_vfs_map [dict get $model runtime_vfs_map]
set vfs_runtime_map [dict get $model vfs_runtime_map] set vfs_runtime_map [dict get $model vfs_runtime_map]
@ -804,7 +808,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
} }
set records [list] set records [list]
set exe_names_seen [list] set exe_names_seen [list]
set record_pair {{rtname vfstail appname target_kit_type} { set record_pair {{rtname vfstail appname target_kit_type smokerequires} {
upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\ upvar 1 records records exe_names_seen exe_names_seen rtbase rtbase sourcefolder sourcefolder\
runtime_target runtime_target default_target default_target runtime_target runtime_target default_target default_target
if {$appname eq ""} { if {$appname eq ""} {
@ -850,6 +854,7 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
store_tier $store_tier\ store_tier $store_tier\
vfs $vfstail\ vfs $vfstail\
vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\ vfs_present [file isdirectory [file join $sourcefolder vfs $vfstail]]\
smokerequire $smokerequires\
] ]
}} }}
foreach vfstail $vfs_tails { foreach vfstail $vfs_tails {
@ -862,25 +867,25 @@ proc ::punkboot::lib::mapvfs_kit_outputs {model rtbase sourcefolder} {
continue continue
} }
foreach vfs_app [dict get $runtime_vfs_map $rtname] { foreach vfs_app [dict get $runtime_vfs_map $rtname] {
lassign $vfs_app configured_vfs appname target_kit_type lassign $vfs_app configured_vfs appname target_kit_type - smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
} }
#entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse) #entries whose vfs folder is missing (excluded from vfs_runtime_map by the parse)
dict for {rtname vfs_specs} $runtime_vfs_map { dict for {rtname vfs_specs} $runtime_vfs_map {
foreach vfsconfig $vfs_specs { foreach vfsconfig $vfs_specs {
if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 4} { if {[llength $vfsconfig] < 1 || [llength $vfsconfig] > 5} {
continue continue
} }
lassign $vfsconfig vfstail appname target_kit_type lassign $vfsconfig vfstail appname target_kit_type - smokerequires
if {[dict exists $vfs_runtime_map $vfstail]} { if {[dict exists $vfs_runtime_map $vfstail]} {
continue ;#already enumerated above continue ;#already enumerated above
} }
apply $record_pair $rtname $vfstail $appname $target_kit_type apply $record_pair $rtname $vfstail $appname $target_kit_type $smokerequires
} }
} }
return $records return $records
@ -2282,6 +2287,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe.vfs src/_build/<kit>.exe.vfs
| |
| advisory payload/target binary-arch scan of the merged
| tree (wrong-arch libraries -> recapped BUILD-WARNING) [K11]
| boot-precondition gate: merged vfs must supply a tcl | boot-precondition gate: merged vfs must supply a tcl
| library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10] | library (tcl_library/, lib/tcl<M>.<m>/, tcl<M>.<m>/) [K10]
v v
@ -2294,6 +2301,8 @@ DIAGRAM 2 - KIT ASSEMBLY DETAIL (the 'make.tcl bake' stage; incl. vfslibs phase)
v v
src/_build/<kit>.exe src/_build/<kit>.exe
| |
| smoke-require probe (host-runnable kits with declared
| packages): plain 'package require' inside the artifact [K11]
| deploy step: delete old bin/<kit>.exe, copy new one in | deploy step: delete old bin/<kit>.exe, copy new one in
v v
bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a bin/<kit>.exe <-- FAILS (kit listed under FAILED KITS) if a
@ -2377,6 +2386,22 @@ KEY / NOTES
own payload could not be extracted - see the BUILD-WARNING naming what was own payload could not be extracted - see the BUILD-WARNING naming what was
tried. 'make.tcl check' reports whether the gate is ACTIVE. tried. 'make.tcl check' reports whether the gate is ACTIVE.
[K11] PAYLOAD/TARGET consistency checks (G-133), both ADVISORY - warnings recap at
end of run, the kit still builds and deploys. (a) Binary-arch scan at the same
seam as [K10]: each binary library (*.dll/*.so/*.dylib) in the merged tree is
classified by header (PE/ELF/Mach-O - structural, nothing executed, so
cross-target kits are covered) against the kit's target platform; wrong-arch
libraries outside platform-discriminated subdirs (win32-ix86/, win-x64/ etc -
multi-arch payloads are legitimate there) earn recapped BUILD-WARNINGs.
(b) Smoke-require probe: a kit declaring packages (mapvfs.config 5th entry
element) has each one plain-'package require'd INSIDE the freshly built
artifact via its tclsh subcommand - the only check that sees resolution-order
defects (a wrong-arch higher-versioned package shadowing a working copy);
cross-target kits skip with a stated reason, undeclared kits run nothing new.
What neither check guarantees: statically linked packages, pure-tcl packages
with binary dependencies, and version-preference outcomes are visible only to
the smoke probe - and only for declared packages. 'make.tcl check' reports both.
MAINTENANCE (agents take note) MAINTENANCE (agents take note)
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
This text is embedded in make.tcl (::punkboot::workflow_text). When the build This text is embedded in make.tcl (::punkboot::workflow_text). When the build
@ -3356,6 +3381,95 @@ proc ::punkboot::get_vfs_boot_library_report {vfsfolder} {
} }
return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]] return [list 1 [::punkboot::utils::vfs_boot_library_report $vfsfolder]]
} }
#Availability probe + advisory payload/target binary-arch scan of an assembled kit vfs
#(G-133), shared by the bake loop and the 'check' command report. Same guarded-require
#treatment as the boot-precondition check above: a stale bootsupport snapshot degrades
#the scan to a NOTE. Returns {available report}.
proc ::punkboot::get_vfs_binary_arch_report {vfsfolder targetplatform} {
set available [expr {\
![catch {package require punkboot::utils}]\
&& [llength [info commands ::punkboot::utils::vfs_binary_arch_report]]\
}]
if {!$available} {
return [list 0 [dict create]]
}
return [list 1 [::punkboot::utils::vfs_binary_arch_report $vfsfolder $targetplatform]]
}
#G-133 smoke-require probe: execute a freshly built kit artifact via its 'tclsh'
#subcommand and plain-'package require' each declared package inside it. This is the
#only check that observes actual package RESOLUTION in the artifact - the punkluck86
#incident class (a higher-versioned wrong-arch Thread shadowing the runtime's working
#copy on plain require) is invisible to every structural check because the working
#file exists. The tclsh subcommand is used deliberately: it exits cleanly even on
#runtimes whose full repl teardown is fragile, and a script argument with drained
#stdin cannot reach any console-reopen path. ADVISORY: failures become recapped
#BUILD-WARNINGs naming kit, package and the actual error - the kit still deploys.
#Returns 1 when every declared package resolved, else 0.
proc ::punkboot::kit_smoke_require_probe {kitpath targetkit packages} {
set probescript [file rootname $kitpath]_smokerequire.tcl
if {[catch {
set fd [open $probescript w]
chan configure $fd -translation lf
puts $fd "#generated by make.tcl bake (G-133) - smoke-require probe for $targetkit"
puts $fd "set packages [list $packages]"
puts $fd {foreach pkg $packages {
if {[catch {package require $pkg} v]} {
puts stdout [list SMOKE-REQUIRE $pkg FAIL $v]
} else {
puts stdout [list SMOKE-REQUIRE $pkg OK $v]
}
}
exit 0}
close $fd
} errM]} {
catch {close $fd}
::punkboot::print_build_warnings [list "smoke-require for kit $targetkit could not write its probe script ($probescript): $errM"]
return 0
}
if {[catch {
lassign [punk::lib::invoke [list [file nativename $kitpath] tclsh [file nativename $probescript] << ""]] p_stdout p_stderr p_exitcode
} errM]} {
::punkboot::print_build_warnings [list "smoke-require FAILED for kit $targetkit: could not execute freshly built artifact via its tclsh subcommand ($errM)"]
return 0
}
set seen [dict create]
foreach line [split $p_stdout \n] {
set line [string trim $line]
if {![string match "SMOKE-REQUIRE *" $line]} {
continue
}
#each line is a well-formed tcl list: SMOKE-REQUIRE <pkg> OK|FAIL <version|error>
if {[catch {lassign $line - r_pkg r_status r_detail}]} {
continue
}
dict set seen $r_pkg [list $r_status $r_detail]
}
set allok 1
set warnings [list]
foreach pkg $packages {
if {![dict exists $seen $pkg]} {
set allok 0
set tail [string trim [join [lrange [split [string trim $p_stderr] \n] end-2 end] " | "]]
lappend warnings "smoke-require FAILED for kit $targetkit: no result for package '$pkg' (artifact exited $p_exitcode before reporting[expr {$tail ne "" ? "; stderr tail: $tail" : ""}])"
continue
}
lassign [dict get $seen $pkg] r_status r_detail
if {$r_status eq "OK"} {
puts stdout " smoke-require OK for kit $targetkit: package require $pkg -> $r_detail"
} else {
set allok 0
lappend warnings "smoke-require FAILED for kit $targetkit: package require $pkg -> $r_detail"
}
}
if {$p_exitcode != 0} {
set allok 0
lappend warnings "smoke-require for kit $targetkit: artifact exited with code $p_exitcode via its tclsh subcommand (expected 0)"
}
if {[llength $warnings]} {
::punkboot::print_build_warnings $warnings
}
return $allok
}
#Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter #Wrap ::exit so accumulated provenance warnings are recapped at the very end of output no matter
#which of make.tcl's many exit points a command takes. #which of make.tcl's many exit points a command takes.
if {![llength [info commands ::punkboot::exit_original]]} { if {![llength [info commands ::punkboot::exit_original]]} {
@ -3612,6 +3726,40 @@ if {$::punkboot::command eq "check"} {
puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)" puts stdout "boot-precondition gate (G-125): UNAVAILABLE (punkboot::utils vfs_boot_library_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'." puts stdout " bake will proceed with a NOTE and cannot refuse an unbootable kit - run 'make.tcl modules' then 'make.tcl bootsupport'."
} }
# Payload/target consistency checks (G-133) - advisory arch scan + smoke-require probe
puts stdout $sep
#availability probe only - the path deliberately does not exist
lassign [::punkboot::get_vfs_binary_arch_report [file join $projectroot __punkboot_archscan_probe__] $::punkboot::target_platform] _arch_available _arch_report
if {$_arch_available} {
puts stdout "payload/target arch scan (G-133): ACTIVE (advisory)"
puts stdout " bake classifies each merged kit's binary libraries (*.dll/*.so/*.dylib header bytes - structural,"
puts stdout " nothing executed, cross-target kits included) against the kit's target platform and emits"
puts stdout " recapped BUILD-WARNINGs for wrong-arch libraries outside platform-discriminated subdirs"
puts stdout " (canonical <os>-<cpu> names and recognised vendor spellings such as win-x64 are exempt)."
} else {
puts stdout "payload/target arch scan (G-133): UNAVAILABLE (punkboot::utils vfs_binary_arch_report not loadable from bootsupport)"
puts stdout " bake will proceed with a NOTE and cannot flag wrong-arch payload libraries - run 'make.tcl modules' then 'make.tcl bootsupport'."
}
set _smoke_declared [list]
if {[file exists $sourcefolder/runtime/mapvfs.config]} {
set _smokemodel [punkboot::lib::mapvfs_parse $sourcefolder/runtime/mapvfs.config [file dirname $sourcefolder]/bin/runtime $sourcefolder $::punkboot::target_platform]
foreach _rec [punkboot::lib::mapvfs_kit_outputs $_smokemodel [file dirname $sourcefolder]/bin/runtime $sourcefolder] {
if {[llength [dict get $_rec smokerequire]]} {
lappend _smoke_declared "[dict get $_rec kitname] ([join [dict get $_rec smokerequire] {, }])"
}
}
unset -nocomplain _smokemodel _rec
}
puts stdout "smoke-require probe (G-133, advisory): freshly built host-runnable kits declaring packages"
puts stdout " (mapvfs.config 5th entry element) have each one plain-'package require'd inside the built"
puts stdout " artifact via its tclsh subcommand - the only check that sees resolution-order defects such as"
puts stdout " a wrong-arch higher-versioned package shadowing a working copy. Failures are recapped"
puts stdout " BUILD-WARNINGs; cross-target kits skip with a stated reason; undeclared kits run nothing new."
if {[llength $_smoke_declared]} {
puts stdout " declared: [join $_smoke_declared {; }]"
} else {
puts stdout " declared: (none)"
}
puts stdout $sep puts stdout $sep
exit 0 exit 0
} }
@ -5840,7 +5988,8 @@ foreach vfstail $vfs_tails {
if {[dict exists $runtime_vfs_map $rtname]} { if {[dict exists $runtime_vfs_map $rtname]} {
set applist [dict get $runtime_vfs_map $rtname] set applist [dict get $runtime_vfs_map $rtname]
foreach vfs_app $applist { foreach vfs_app $applist {
lassign $vfs_app configured_vfs appname target_kit_type #5th element = G-133 smoke-require package list ("" when undeclared)
lassign $vfs_app configured_vfs appname target_kit_type - kit_smokerequires
if {$configured_vfs ne $vfstail} { if {$configured_vfs ne $vfstail} {
continue continue
} }
@ -5862,7 +6011,7 @@ foreach vfstail $vfs_tails {
} }
} }
lappend exe_names_seen $targetkit lappend exe_names_seen $targetkit
lappend targetkits [list $targetkit $target_kit_type] lappend targetkits [list $targetkit $target_kit_type $kit_smokerequires]
} }
} }
puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits" puts stdout " vfs: $vfstail runtime: $rtname targetkits: $targetkits"
@ -5871,7 +6020,7 @@ foreach vfstail $vfs_tails {
continue ;#G-121 selective bake - not a requested kit output continue ;#G-121 selective bake - not a requested kit output
} }
puts stdout " processing targetkit: $targetkit_info" puts stdout " processing targetkit: $targetkit_info"
lassign $targetkit_info targetkit target_kit_type lassign $targetkit_info targetkit target_kit_type kit_smokerequires
#Self-build guard: never process the kit whose deployed executable is running this #Self-build guard: never process the kit whose deployed executable is running this
#build - the process sweep below would kill this very process, and the deploy step #build - the process sweep below would kill this very process, and the deploy step
#cannot replace a running image anyway. Informational/update subcommands are #cannot replace a running image anyway. Informational/update subcommands are
@ -6240,6 +6389,37 @@ foreach vfstail $vfs_tails {
set sourcevfs [file join $sourcefolder vfs $vfstail] set sourcevfs [file join $sourcefolder vfs $vfstail]
merge_over $sourcevfs $targetvfs merge_over $sourcevfs $targetvfs
#G-133 advisory payload/target binary-arch scan, at the same post-merge
#seam as the G-125 gate below. Structural (header bytes only, nothing
#executed), so it runs for every kit including cross-target ones. A
#binary library under a platform-discriminated subdir (win32-ix86/,
#win-x64/ etc) is exempt - multi-arch payloads are legitimate there.
#ADVISORY by design: vendor layouts make false certainty unavoidable,
#so mismatches warn (recapped) and the kit still builds - the G-125
#gate below stays the only refusal at this seam.
lassign [::punkboot::get_vfs_binary_arch_report $targetvfs $rt_target] archscan_available archscan
if {!$archscan_available} {
puts stderr "NOTE: kit payload binary-arch scan unavailable (punkboot::utils vfs_binary_arch_report not loadable from bootsupport) - continuing without it"
} elseif {[dict get $archscan reason] ne ""} {
puts stdout " payload binary-arch scan not applicable for kit $targetkit: [dict get $archscan reason]"
} elseif {[llength [dict get $archscan mismatches]]} {
set archscan_mismatches [dict get $archscan mismatches]
set archwarnings [list]
set shown 0
foreach m $archscan_mismatches {
if {$shown >= 8} {break}
incr shown
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [dict get $m file] is [dict get $m detail] outside any platform-discriminated subdir - a $rt_target runtime cannot load it"
}
if {[llength $archscan_mismatches] > $shown} {
lappend archwarnings "payload/target arch mismatch in kit $targetkit (target $rt_target): [expr {[llength $archscan_mismatches] - $shown}] further mismatching binary libraries not listed above ([llength $archscan_mismatches] mismatches total, [dict get $archscan scanned] binary libraries scanned)"
}
::punkboot::print_build_warnings $archwarnings
unset archwarnings archscan_mismatches shown
} else {
puts stdout " payload binary-arch scan for kit $targetkit (target $rt_target): OK - [dict get $archscan scanned] binary libraries scanned ([dict get $archscan matched] match target, [dict get $archscan exempt] exempt under platform subdirs, [dict get $archscan unknown] unclassifiable)"
}
#G-125 boot-precondition gate. A kit with no tcl library cannot initialise #G-125 boot-precondition gate. A kit with no tcl library cannot initialise
#at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any #at all - 'Cannot find a usable init.tcl' - so refuse it HERE: before any
#build product is written, and long before the deploy step deletes the #build product is written, and long before the deploy step deletes the
@ -6588,6 +6768,23 @@ foreach vfstail $vfs_tails {
# -- --- --- --- --- --- # -- --- --- --- --- ---
$vfs_event targetset_end OK $vfs_event targetset_end OK
#G-133 post-build smoke-require probe. Only an actual 'package require'
#inside the built artifact observes package RESOLUTION - the wrong-arch
#version-shadowing class is invisible to every structural check because
#the working file exists too. Runs only for kits that declare
#smoke-require packages (mapvfs.config 5th entry element) and only when
#this host can execute the artifact; undeclared kits run nothing new.
if {[llength $kit_smokerequires]} {
if {$rtname eq "-"} {
puts stdout " smoke-require skipped for kit $targetkit: a runtimeless .kit artifact is not directly executable"
} elseif {[punkboot::lib::platform_process_family $rt_target] ne [punkboot::lib::platform_process_family $::punkboot::host_platform]} {
puts stdout " smoke-require skipped for kit $targetkit: cross-target ($rt_target) - the artifact is not executable on this $::punkboot::host_platform host"
} else {
puts stdout " smoke-require probe for kit $targetkit (packages: $kit_smokerequires) - executing freshly built artifact via its tclsh subcommand"
::punkboot::kit_smoke_require_probe $buildfolder/$targetkit $targetkit $kit_smokerequires
}
}
} else { } else {
set skipped_vfs_build 1 set skipped_vfs_build 1
puts stderr "." puts stderr "."

BIN
src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

352
src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.4.0.tm

@ -1,352 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-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) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.4.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_boot_library_report
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vcs_dirty_warnings
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\
-summary\
"Warning lines for uncommitted VCS changes above a path"\
-help\
"Walks up from path to the nearest fossil and/or git root and
returns warning lines describing uncommitted changes.
Artifacts built from a dirty source tree have no committed
provenance - callers such as make.tcl vendorupdate use this
to flag source projects that should be committed first.
checkedrootsvar names a dict variable in the caller's scope
used to report each VCS root at most once across calls.
label, if supplied, is prefixed into each warning to name
the calling operation (e.g. 'vendorupdate').
scope, if supplied, is a subpath relative to path; only
uncommitted changes under that subpath are counted (e.g.
scope 'src' warns about dirty src/ while ignoring dirt
elsewhere in the checkout).
Returns an empty list if the path is clean, unversioned,
missing, already reported, or state cannot be determined."
@leaders
path -type string -optional 0 -help\
"Path to check; the nearest enclosing fossil/git root is examined"
checkedrootsvar -type string -optional 0 -help\
"Name of a dict variable in the caller's scope for dedupe across calls"
label -type string -optional 1 -default "" -help\
"Operation name prefixed into warnings"
scope -type string -optional 1 -default "" -help\
"Subpath relative to path limiting which changes count; empty = whole checkout"
}]
}
proc vcs_dirty_warnings {path checkedrootsvar {label ""} {scope ""}} {
upvar 1 $checkedrootsvar checkedroots
if {![info exists checkedroots]} {set checkedroots [dict create]}
if {$label ne ""} {set label "$label "}
set warnings [list]
set dir [file normalize $path]
if {![file isdirectory $dir]} {
return $warnings ;#missing path - not this proc's business to report
}
set scopeabs ""
set scopedesc "" ;#included in warning text when scoped
if {$scope ne ""} {
set scopeabs [file normalize [file join $dir $scope]]
set scopedesc " under [string trimright $scope /]/"
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot,$scope] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot,$scope 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} else {
set flines [list]
foreach line [split $fchanges \n] {
set line [string trim $line]
if {$line eq ""} {continue}
if {$scopeabs ne ""} {
#fossil changes lines are '<STATUS> <path-relative-to-checkout-root>'
if {![regexp {^\S+\s+(.*)$} $line _ relfile]} {continue}
set normfile [file normalize [file join $fossilroot $relfile]]
if {$normfile ne $scopeabs && ![string match "${scopeabs}/*" $normfile]} {continue}
}
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot,$scope] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot,$scope 1
set gitpathargs [list]
if {$scopeabs ne ""} {
set gitpathargs [list -- $scopeabs]
}
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain {*}$gitpathargs]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
}
#Companion files of a real Tcl library directory. init.tcl sources these, so a
#directory holding init.tcl with none of them beside it is a package's own init
#script (lib/BWidget1.10.1/init.tcl and friends), not a tcl library.
variable boot_library_companions {tm.tcl package.tcl auto.tcl clock.tcl history.tcl word.tcl}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_boot_library_report
@cmd -name "::punkboot::utils::vfs_boot_library_report"\
-summary\
"Report whether an assembled vfs tree can supply a bootable tcl library"\
-help\
"Structural check of a merged .vfs tree for the precondition a kit
needs to initialise at all: a Tcl library the runtime can find. Reads
the directory only - nothing is executed, decompressed or launched, so
a caller such as make.tcl can run it for every kit on every bake and
for a cross-target kit it could never run itself.
Three conventions are recognised, all observed in real runtimes:
tcl_library/ zipfs-attached kits (tclZipfs looks for
<mount>/tcl_library/init.tcl)
lib/tcl<M>.<m>/ starkit/tclkit-style kits (lib/tcl8.6,
lib/tcl9.0)
tcl<M>.<m>/ runtimes whose archive mounts at the
executable's own path rather than at
//zipfs:/app, so [info library] is
<exe>/tcl8.6 - the androwish/undroidwish
zipfs backport for Tcl 8.6 does this
A candidate directory qualifies only when it holds init.tcl AND at
least one companion file a real Tcl library carries beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or
an encoding/ subdirectory. Without that second test a package's own
init script - lib/BWidget1.10.1/init.tcl, present in every punkshell
kit - would answer for a tcl library it cannot provide.
Returned dict keys:
ok 1 when at least one qualifying library was found
locations qualifying directories, relative to vfsfolder
rejected candidate directories that held init.tcl but no
companion, relative to vfsfolder - the near-misses
worth naming when a gate fires unexpectedly
checked the location patterns examined, relative to vfsfolder
reason why ok is 0; empty string when ok is 1"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to inspect"
}]
}
proc vfs_boot_library_report {vfsfolder} {
variable boot_library_companions
set checked [list tcl_library lib/tcl<M>.<m> tcl<M>.<m>]
set report [dict create ok 0 locations {} rejected {} checked $checked reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report reason "no such directory: $vfsfolder"
return $report
}
#candidates are the three conventions only - a full tree walk would find every
#package's init.tcl and cost more than the check is worth.
#The tcl[0-9]* globs deliberately also match module trees named tcl8/ or tcl9/
#(both exist in this project's kits); they carry no init.tcl, so the qualification
#test below rejects them without needing a narrower pattern.
set candidates [list [file join $vfsfolder tcl_library]]
foreach dir [lsort [glob -nocomplain -type d -directory [file join $vfsfolder lib] -- {tcl[0-9]*}]] {
lappend candidates $dir
}
foreach dir [lsort [glob -nocomplain -type d -directory $vfsfolder -- {tcl[0-9]*}]] {
lappend candidates $dir
}
set locations [list]
set rejected [list]
foreach dir $candidates {
if {![file isfile [file join $dir init.tcl]]} {
continue
}
set qualifies 0
foreach companion $boot_library_companions {
if {[file isfile [file join $dir $companion]]} {
set qualifies 1
break
}
}
if {!$qualifies && [file isdirectory [file join $dir encoding]]} {
set qualifies 1
}
set rel [string range $dir [expr {[string length $vfsfolder] + 1}] end]
if {$qualifies} {
lappend locations $rel
} else {
lappend rejected $rel
}
}
if {[llength $locations]} {
dict set report ok 1
dict set report locations $locations
dict set report rejected $rejected
return $report
}
dict set report rejected $rejected
if {[llength $rejected]} {
dict set report reason "no tcl library in [file tail $vfsfolder]: [join $rejected {, }] hold init.tcl but none of the companion files a tcl library carries beside it ([join $boot_library_companions {, }]) nor an encoding/ directory"
} else {
dict set report reason "no tcl library in [file tail $vfsfolder]: none of tcl_library/init.tcl, lib/tcl<major>.<minor>/init.tcl or tcl<major>.<minor>/init.tcl is present"
}
return $report
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.4.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.4.0
}]
return

753
src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.5.0.tm

@ -0,0 +1,753 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-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) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.5.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export binary_arch_classify parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings vfs_binary_arch_report vfs_boot_library_report
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vcs_dirty_warnings
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\
-summary\
"Warning lines for uncommitted VCS changes above a path"\
-help\
"Walks up from path to the nearest fossil and/or git root and
returns warning lines describing uncommitted changes.
Artifacts built from a dirty source tree have no committed
provenance - callers such as make.tcl vendorupdate use this
to flag source projects that should be committed first.
checkedrootsvar names a dict variable in the caller's scope
used to report each VCS root at most once across calls.
label, if supplied, is prefixed into each warning to name
the calling operation (e.g. 'vendorupdate').
scope, if supplied, is a subpath relative to path; only
uncommitted changes under that subpath are counted (e.g.
scope 'src' warns about dirty src/ while ignoring dirt
elsewhere in the checkout).
Returns an empty list if the path is clean, unversioned,
missing, already reported, or state cannot be determined."
@leaders
path -type string -optional 0 -help\
"Path to check; the nearest enclosing fossil/git root is examined"
checkedrootsvar -type string -optional 0 -help\
"Name of a dict variable in the caller's scope for dedupe across calls"
label -type string -optional 1 -default "" -help\
"Operation name prefixed into warnings"
scope -type string -optional 1 -default "" -help\
"Subpath relative to path limiting which changes count; empty = whole checkout"
}]
}
proc vcs_dirty_warnings {path checkedrootsvar {label ""} {scope ""}} {
upvar 1 $checkedrootsvar checkedroots
if {![info exists checkedroots]} {set checkedroots [dict create]}
if {$label ne ""} {set label "$label "}
set warnings [list]
set dir [file normalize $path]
if {![file isdirectory $dir]} {
return $warnings ;#missing path - not this proc's business to report
}
set scopeabs ""
set scopedesc "" ;#included in warning text when scoped
if {$scope ne ""} {
set scopeabs [file normalize [file join $dir $scope]]
set scopedesc " under [string trimright $scope /]/"
}
set fossilroot ""
set gitroot ""
while {1} {
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} {
set fossilroot $dir
}
if {$gitroot eq "" && [file exists [file join $dir .git]]} {
set gitroot $dir
}
if {$fossilroot ne "" || $gitroot ne ""} {
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot,$scope] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot,$scope 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} else {
set flines [list]
foreach line [split $fchanges \n] {
set line [string trim $line]
if {$line eq ""} {continue}
if {$scopeabs ne ""} {
#fossil changes lines are '<STATUS> <path-relative-to-checkout-root>'
if {![regexp {^\S+\s+(.*)$} $line _ relfile]} {continue}
set normfile [file normalize [file join $fossilroot $relfile]]
if {$normfile ne $scopeabs && ![string match "${scopeabs}/*" $normfile]} {continue}
}
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot,$scope] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot,$scope 1
set gitpathargs [list]
if {$scopeabs ne ""} {
set gitpathargs [list -- $scopeabs]
}
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain {*}$gitpathargs]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
}
return $warnings
}
#Companion files of a real Tcl library directory. init.tcl sources these, so a
#directory holding init.tcl with none of them beside it is a package's own init
#script (lib/BWidget1.10.1/init.tcl and friends), not a tcl library.
variable boot_library_companions {tm.tcl package.tcl auto.tcl clock.tcl history.tcl word.tcl}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_boot_library_report
@cmd -name "::punkboot::utils::vfs_boot_library_report"\
-summary\
"Report whether an assembled vfs tree can supply a bootable tcl library"\
-help\
"Structural check of a merged .vfs tree for the precondition a kit
needs to initialise at all: a Tcl library the runtime can find. Reads
the directory only - nothing is executed, decompressed or launched, so
a caller such as make.tcl can run it for every kit on every bake and
for a cross-target kit it could never run itself.
Three conventions are recognised, all observed in real runtimes:
tcl_library/ zipfs-attached kits (tclZipfs looks for
<mount>/tcl_library/init.tcl)
lib/tcl<M>.<m>/ starkit/tclkit-style kits (lib/tcl8.6,
lib/tcl9.0)
tcl<M>.<m>/ runtimes whose archive mounts at the
executable's own path rather than at
//zipfs:/app, so [info library] is
<exe>/tcl8.6 - the androwish/undroidwish
zipfs backport for Tcl 8.6 does this
A candidate directory qualifies only when it holds init.tcl AND at
least one companion file a real Tcl library carries beside it
(tm.tcl, package.tcl, auto.tcl, clock.tcl, history.tcl, word.tcl) or
an encoding/ subdirectory. Without that second test a package's own
init script - lib/BWidget1.10.1/init.tcl, present in every punkshell
kit - would answer for a tcl library it cannot provide.
Returned dict keys:
ok 1 when at least one qualifying library was found
locations qualifying directories, relative to vfsfolder
rejected candidate directories that held init.tcl but no
companion, relative to vfsfolder - the near-misses
worth naming when a gate fires unexpectedly
checked the location patterns examined, relative to vfsfolder
reason why ok is 0; empty string when ok is 1"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to inspect"
}]
}
proc vfs_boot_library_report {vfsfolder} {
variable boot_library_companions
set checked [list tcl_library lib/tcl<M>.<m> tcl<M>.<m>]
set report [dict create ok 0 locations {} rejected {} checked $checked reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report reason "no such directory: $vfsfolder"
return $report
}
#candidates are the three conventions only - a full tree walk would find every
#package's init.tcl and cost more than the check is worth.
#The tcl[0-9]* globs deliberately also match module trees named tcl8/ or tcl9/
#(both exist in this project's kits); they carry no init.tcl, so the qualification
#test below rejects them without needing a narrower pattern.
set candidates [list [file join $vfsfolder tcl_library]]
foreach dir [lsort [glob -nocomplain -type d -directory [file join $vfsfolder lib] -- {tcl[0-9]*}]] {
lappend candidates $dir
}
foreach dir [lsort [glob -nocomplain -type d -directory $vfsfolder -- {tcl[0-9]*}]] {
lappend candidates $dir
}
set locations [list]
set rejected [list]
foreach dir $candidates {
if {![file isfile [file join $dir init.tcl]]} {
continue
}
set qualifies 0
foreach companion $boot_library_companions {
if {[file isfile [file join $dir $companion]]} {
set qualifies 1
break
}
}
if {!$qualifies && [file isdirectory [file join $dir encoding]]} {
set qualifies 1
}
set rel [string range $dir [expr {[string length $vfsfolder] + 1}] end]
if {$qualifies} {
lappend locations $rel
} else {
lappend rejected $rel
}
}
if {[llength $locations]} {
dict set report ok 1
dict set report locations $locations
dict set report rejected $rejected
return $report
}
dict set report rejected $rejected
if {[llength $rejected]} {
dict set report reason "no tcl library in [file tail $vfsfolder]: [join $rejected {, }] hold init.tcl but none of the companion files a tcl library carries beside it ([join $boot_library_companions {, }]) nor an encoding/ directory"
} else {
dict set report reason "no tcl library in [file tail $vfsfolder]: none of tcl_library/init.tcl, lib/tcl<major>.<minor>/init.tcl or tcl<major>.<minor>/init.tcl is present"
}
return $report
}
#G-133 binary-arch scan support. Recognition data is deliberately plain namespace
#variables so extending it (a new vendor spelling, a new cpu token) is a visible
#one-line edit rather than a code change.
#
#PE Machine field values (IMAGE_FILE_HEADER.Machine, 2 bytes LE at e_lfanew+4)
variable pe_machine_map {
014c i386
8664 x86_64
aa64 arm64
01c0 arm
01c4 arm
0200 ia64
0166 mips
01f0 ppc
5032 riscv32
5064 riscv64
}
#ELF e_machine values (2 bytes at offset 18, endianness per EI_DATA)
variable elf_machine_map {
3 i386
62 x86_64
183 arm64
40 arm
8 mips
20 ppc
21 ppc64
22 s390x
2 sparc
43 sparc64
243 riscv
}
#Mach-O cputype base values (CPU_ARCH_ABI64 flag 0x01000000 selects the 64-bit variant)
variable macho_cputype_map {
7 {i386 x86_64}
12 {arm arm64}
18 {ppc ppc64}
}
#os / cpu tokens accepted in a CANONICAL platform-dir segment (<os>-<cpu>). The os and
#cpu lists follow the punk::platform canon plus common alias tokens, so a platform-
#discriminated subdir is recognised whichever spelling a package author normalised to.
variable platform_segment_os {
win32 windows linux macosx freebsd netbsd openbsd dragonfly solaris sunos
aix hpux irix qnx haiku android msys cygwin
}
variable platform_segment_cpu {
x86_64 amd64 x64 ix86 i386 i486 i586 i686 x86
arm64 aarch64 arm armv6 armv7 armv6l armv7l
ppc ppc64 ppc64le powerpc mips mipsel mips64
riscv32 riscv64 s390 s390x sparc sparc64 universal
}
#Recognised VENDOR spellings observed (or near-certain) as platform-discriminating
#directory names that do not parse as <canonical os>-<canonical cpu>. blend2d,
#extrafont and tclMuPDF ship win-x64 today.
variable platform_segment_vendor {
win-x64 win-x86 win-arm64 win-arm win32 win64
linux-x64 linux-arm64 macos-x64 macos-arm64 osx-x64 osx-arm64
}
#File tails treated as binary libraries by vfs_binary_arch_report
variable binary_library_globs {*.dll *.so *.so.[0-9]* *.dylib}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::binary_arch_classify
@cmd -name "::punkboot::utils::binary_arch_classify"\
-summary\
"Classify a binary library file's executable format and cpu architecture"\
-help\
"Reads only the file's header bytes (a couple of small reads - nothing
is executed or loaded) and classifies the executable format and cpu
architecture:
PE Machine field (2 bytes at e_lfanew+4): i386, x86_64, arm64...
ELF e_machine honouring EI_DATA endianness
Mach-O thin (cputype) and fat/universal (per-member cputypes)
Unrecognised content is reported honestly as format 'unknown' rather
than guessed.
Returned dict keys:
format pe | elf | macho | unknown
arch i386 | x86_64 | arm64 | ... | universal | unknown |
unknown-0x<hex> (a real header whose machine value is not
in the recognition map - definite, just unmapped)
archs member architectures (macho universal only; absent otherwise)
detail short human-readable classification"
@leaders
libfile -type string -optional 0 -help\
"Path of the binary library file to classify"
}]
}
proc binary_arch_classify {libfile} {
variable pe_machine_map
variable elf_machine_map
variable macho_cputype_map
if {[catch {
set fd [open $libfile r]
chan configure $fd -translation binary
set head [read $fd 64]
} errM]} {
catch {close $fd}
return [dict create format unknown arch unknown detail "unreadable: $errM"]
}
set result [dict create format unknown arch unknown detail "not a recognised binary library format"]
try {
if {[string length $head] < 8} {
dict set result detail "too short to carry an executable header ([string length $head] bytes)"
return $result
}
set magic4 [string range $head 0 3]
if {[string range $head 0 1] eq "MZ"} {
#PE: e_lfanew at 0x3c points at "PE\0\0" + IMAGE_FILE_HEADER
if {[binary scan $head @60iu e_lfanew] != 1} {
dict set result detail "MZ header too short for e_lfanew"
return $result
}
seek $fd $e_lfanew
set pehdr [read $fd 6]
if {[string length $pehdr] < 6 || [string range $pehdr 0 3] ne "PE\x00\x00"} {
dict set result detail "MZ header without PE signature (DOS executable?)"
return $result
}
binary scan $pehdr @4su machine
set hexmachine [format %04x $machine]
if {[dict exists $pe_machine_map $hexmachine]} {
set arch [dict get $pe_machine_map $hexmachine]
set result [dict create format pe arch $arch detail "PE $arch"]
} else {
set result [dict create format pe arch unknown-0x$hexmachine detail "PE machine 0x$hexmachine (not in recognition map)"]
}
} elseif {$magic4 eq "\x7fELF"} {
binary scan $head @4cu elfclass
binary scan $head @5cu elfdata
if {$elfdata == 2} {
binary scan $head @18Su e_machine ;#big-endian
} else {
binary scan $head @18su e_machine ;#little-endian (and honest-enough default)
}
if {[dict exists $elf_machine_map $e_machine]} {
set arch [dict get $elf_machine_map $e_machine]
if {$arch eq "riscv"} {
set arch [expr {$elfclass == 2 ? "riscv64" : "riscv32"}]
}
set result [dict create format elf arch $arch detail "ELF $arch"]
} else {
set result [dict create format elf arch unknown-$e_machine detail "ELF e_machine $e_machine (not in recognition map)"]
}
} else {
binary scan $head Iu be32
switch -- [format %08x $be32] {
feedface - feedfacf - cefaedfe - cffaedfe {
#thin Mach-O; cputype follows the magic in the file's own byte order
if {[string index $head 0] eq "\xfe"} {
binary scan $head @4Iu cputype ;#big-endian file
} else {
binary scan $head @4iu cputype
}
set is64 [expr {($cputype & 0x01000000) != 0}]
set base [expr {$cputype & 0x00ffffff}]
if {[dict exists $macho_cputype_map $base]} {
set arch [lindex [dict get $macho_cputype_map $base] $is64]
set result [dict create format macho arch $arch detail "Mach-O $arch"]
} else {
set result [dict create format macho arch unknown-cputype-$base detail "Mach-O cputype $base (not in recognition map)"]
}
}
cafebabe - cafebabf {
#fat/universal (big-endian header). A Java class file shares the
#magic - its version field where nfat_arch sits is >= 45, far above
#any credible member count, so an implausible count stays unknown.
binary scan $head @4Iu nfat
if {$nfat == 0 || $nfat > 30} {
dict set result detail "cafebabe magic with implausible member count $nfat (java class file?)"
return $result
}
set entrysize [expr {$be32 == 0xcafebabf ? 32 : 20}]
seek $fd 8
set fatdata [read $fd [expr {$nfat * $entrysize}]]
set archs [list]
for {set i 0} {$i < $nfat} {incr i} {
if {[binary scan $fatdata @[expr {$i * $entrysize}]Iu cputype] != 1} {
break
}
set is64 [expr {($cputype & 0x01000000) != 0}]
set base [expr {$cputype & 0x00ffffff}]
if {[dict exists $macho_cputype_map $base]} {
lappend archs [lindex [dict get $macho_cputype_map $base] $is64]
} else {
lappend archs unknown-cputype-$base
}
}
set result [dict create format macho arch universal archs $archs detail "Mach-O universal ([join $archs {, }])"]
}
}
}
return $result
} finally {
catch {close $fd}
}
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::platform_expected_binary
@cmd -name "::punkboot::utils::platform_expected_binary"\
-summary\
"Expected binary format/arch for a canonical target platform name"\
-help\
"Maps a canonical punkshell platform name (<os>-<cpu>, e.g
win32-ix86) to the executable format and classifier arch token a
native binary library for that platform carries. Returns a dict
{format F arch A}, or an empty dict when the os or cpu token has
no recognised mapping (callers treat that as scan-not-applicable
rather than guessing)."
@leaders
targetplatform -type string -optional 0 -help\
"Canonical platform name, e.g win32-x86_64, linux-x86_64, macosx-arm64"
}]
}
proc platform_expected_binary {targetplatform} {
set idx [string first - $targetplatform]
if {$idx <= 0} {
return [dict create]
}
set os [string range $targetplatform 0 [expr {$idx - 1}]]
set cpu [string range $targetplatform [expr {$idx + 1}] end]
switch -- $os {
win32 - windows {set format pe}
linux - freebsd - netbsd - openbsd - dragonfly - solaris - sunos {set format elf}
macosx - darwin - macos {set format macho}
default {return [dict create]}
}
switch -- $cpu {
ix86 - i386 - i486 - i586 - i686 - x86 {set arch i386}
x86_64 - amd64 - x64 {set arch x86_64}
arm64 - aarch64 {set arch arm64}
arm - armv6 - armv7 {set arch arm}
ppc - ppc64 - riscv64 - mips - s390x - sparc - sparc64 {set arch $cpu}
default {return [dict create]}
}
return [dict create format $format arch $arch]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::platform_discriminated_segment
@cmd -name "::punkboot::utils::platform_discriminated_segment"\
-summary\
"Whether a directory name is a recognised platform-discriminating segment"\
-help\
"True when the segment is a canonical <os>-<cpu> platform-dir name
(os and cpu each from the recognised token lists), the bare
universal 'macosx' folder, or one of the recognised vendor
spellings (blend2d-style win-x64 etc). Binary libraries under such
a directory are selected per-platform by their package's own index
logic, so a multi-arch payload is legitimate there."
@leaders
segment -type string -optional 0 -help\
"One directory name (a single path segment, not a path)"
}]
}
proc platform_discriminated_segment {segment} {
variable platform_segment_os
variable platform_segment_cpu
variable platform_segment_vendor
set segment [string tolower $segment]
if {$segment eq "macosx"} {
return 1 ;#the universal store-dir convention
}
if {$segment in $platform_segment_vendor} {
return 1
}
set idx [string first - $segment]
if {$idx > 0} {
set os [string range $segment 0 [expr {$idx - 1}]]
set cpu [string range $segment [expr {$idx + 1}] end]
if {$os in $platform_segment_os && $cpu in $platform_segment_cpu} {
return 1
}
}
return 0
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::vfs_binary_arch_report
@cmd -name "::punkboot::utils::vfs_binary_arch_report"\
-summary\
"Advisory scan of an assembled vfs tree for binary libraries that mismatch the kit's target"\
-help\
"Walks a merged .vfs tree, classifies each binary library file
(*.dll, *.so, *.so.<n>, *.dylib) by header (binary_arch_classify)
and compares it against the kit's declared target platform. A
library under a platform-discriminated subdirectory (canonical
<os>-<cpu> names, the universal macosx folder, or a recognised
vendor spelling such as win-x64) is EXEMPT - multi-arch payloads
are legitimate when a platform segment lets the package's own
index logic pick per platform. Purely structural: header bytes are
read, nothing is executed or loaded, so the scan also covers
cross-target kits the build host could never run.
A mismatch is reported only when the classification is DEFINITE
(a real PE/ELF/Mach-O header whose format or architecture differs
from the target's); unclassifiable files are counted as unknown,
never warned about. What this cannot see: statically linked
packages, pure-tcl packages whose binary dependency is elsewhere,
and package resolution order (a wrong-arch copy shadowing a
working one by version) - only an in-artifact 'package require'
smoke probe observes those.
Returned dict keys:
ok 1 when no mismatches were found (advisory)
target the target platform as supplied
expected {format F arch A} for the target ({} if unmapped)
scanned binary library files classified
matched files matching the expected format/arch
exempt files under platform-discriminated subdirs (not read)
unknown files whose classification was indefinite
mismatches list of {file <rel> format F arch A detail D}
reason why the scan did not apply; empty when it ran"
@leaders
vfsfolder -type string -optional 0 -help\
"Path of the assembled vfs tree to scan"
targetplatform -type string -optional 0 -help\
"Canonical target platform of the kit, e.g win32-ix86"
}]
}
proc vfs_binary_arch_report {vfsfolder targetplatform} {
variable binary_library_globs
set report [dict create ok 1 target $targetplatform expected {} scanned 0 matched 0 exempt 0 unknown 0 mismatches {} reason ""]
if {![file isdirectory $vfsfolder]} {
dict set report ok 0
dict set report reason "no such directory: $vfsfolder"
return $report
}
set expected [platform_expected_binary $targetplatform]
dict set report expected $expected
if {![dict size $expected]} {
dict set report reason "target platform '$targetplatform' has no recognised binary format/arch mapping - scan not applicable"
return $report
}
set expected_format [dict get $expected format]
set expected_arch [dict get $expected arch]
set scanned 0
set matched 0
set exempt 0
set unknown 0
set mismatches [list]
set rootlen [string length $vfsfolder]
#iterative walk - plain Tcl 8.6, no deps. Exemption is decided per DIRECTORY
#(one platform_discriminated_segment test as each dir is queued), so files
#under an exempt subtree are counted without being read.
set pending [list [list $vfsfolder 0]]
while {[llength $pending]} {
set entry [lindex $pending end]
set pending [lrange $pending 0 end-1]
lassign $entry dir dir_exempt
foreach sub [glob -nocomplain -types d -directory $dir *] {
set sub_exempt $dir_exempt
if {!$sub_exempt && [platform_discriminated_segment [file tail $sub]]} {
set sub_exempt 1
}
lappend pending [list $sub $sub_exempt]
}
foreach libfile [glob -nocomplain -types f -directory $dir -- {*}$binary_library_globs] {
if {$dir_exempt} {
incr exempt
continue
}
incr scanned
set class [binary_arch_classify $libfile]
set cformat [dict get $class format]
set carch [dict get $class arch]
if {$cformat eq "unknown"} {
incr unknown
continue
}
set rel [string map {\\ /} [string range $libfile [expr {$rootlen + 1}] end]]
if {$cformat ne $expected_format} {
lappend mismatches [dict create file $rel format $cformat arch $carch detail [dict get $class detail]]
continue
}
if {$carch eq $expected_arch} {
incr matched
continue
}
if {$carch eq "universal" && [dict exists $class archs] && $expected_arch in [dict get $class archs]} {
incr matched
continue
}
#definite finding either way: a differing mapped arch, or a real header
#whose unmapped machine value is provably not the expected architecture
lappend mismatches [dict create file $rel format $cformat arch $carch detail [dict get $class detail]]
}
}
dict set report scanned $scanned
dict set report matched $matched
dict set report exempt $exempt
dict set report unknown $unknown
dict set report mismatches $mismatches
dict set report ok [expr {[llength $mismatches] == 0}]
return $report
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.5.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.5.0
}]
return
Loading…
Cancel
Save