diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b870d1..3ebb98ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.10.3] - 2026-07-11 + +- make.tcl build/promotion commands (`project`, `packages`, `modules`, `libs`, `vfs`, `vfslibs`, `bin`, `bootsupport`, `vfscommonupdate`) now warn when `src/` has uncommitted fossil/git changes — artifacts built from dirty src have no committed provenance (G-026 direction). Warn-only by default; a new `-dirty-abort` flag makes the check aborting. Dirt outside `src/` is ignored (`punkboot::utils::vcs_dirty_warnings` gained an optional scope argument, utils 0.2.0). With ` src` / ` src shell` available for evaluating uncommitted source directly, building is the promotion step the warning treats it as. +- Provenance warnings (dirty-src gate and vendorupdate's dirty source-project check) are presented with a plain column-0 `PROVENANCE-WARNING:` token for automated discovery in redirected output plus ANSI colour for humans, and are recapped at the very end of the run (via a wrapped `::exit`) so they survive scrolling chatty build output. Interactive terminal runs (Tcl 8.7+/9 stdin `-inputmode` probe) additionally get a 3-second ctrl-c grace countdown before a dirty build proceeds; piped/agent/CI runs pay no delay. `make.tcl check` now reports src provenance status and what the build/promotion commands would do. + ## [0.10.2] - 2026-07-11 - punk::libunknown bumped 0.1 → 0.2.0 (file renamed `libunknown-0.2.0.tm`), retroactively versioning the register_all_tm and source_pkgindex additions and adopting full `major.minor.patch` form. Verified nothing requires it by exact version or filename: punk_main.tcl and punk::repl glob `libunknown-*.tm` picking the highest by vcompare (the dev copy now also outversions the stale bootsupport/project.vfs `0.1` copies deterministically), bootsupport's include_modules.config lists it by name only. The module header now carries a version-history block in lieu of a buildversion.txt, and `src/modules/AGENTS.md` documents the manual-versioning bump mechanics for agents (same Patch/Minor/Major rules as buildversion-tracked modules). diff --git a/punkproject.toml b/punkproject.toml index ef3246a8..405c61b9 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.10.2" +version = "0.10.3" diff --git a/src/AGENTS.md b/src/AGENTS.md index c22a10bd..a85a45dd 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -64,6 +64,8 @@ Recovery after a wrong path guess: - Use `tclsh src/make.tcl libs` to build just the library packages. - Use `tclsh src/make.tcl packages` to build both modules and libraries. - Use `tclsh src/make.tcl vendorupdate` to refresh vendormodules from config. It warns (non-fatal) for each source project whose fossil/git checkout is dirty - vendored artifacts built from a dirty tree have no committed provenance; commit in the source project to clear the warning (enforcement policy tracked by goal G-026). +- All build/promotion commands (`project`, `packages`, `modules`, `libs`, `vfs`, `vfslibs`, `bin`, `bootsupport`, `vfscommonupdate`) warn similarly when this project's own `src/` has uncommitted fossil/git changes (dirt outside `src/` is ignored). Warn-only by default; pass `-dirty-abort` to make the check aborting. For evaluating uncommitted source without a build, prefer ` src` / ` src shell`. The check is `punkboot::utils::vcs_dirty_warnings` (optional scope argument) loaded guardedly from bootsupport - if the snapshot is stale/missing the check degrades to a skip notice (but `-dirty-abort` then aborts rather than silently losing the requested strictness). +- Provenance warnings (dirty-src gate + vendorupdate source-project check) print with a plain column-0 `PROVENANCE-WARNING:` token (grep for it in captured build output) and are recapped at the end of the run via make.tcl's wrapped `::exit`. Interactive terminal runs get a 3-second ctrl-c grace countdown before a dirty build proceeds; piped/agent runs skip it. `tclsh src/make.tcl check` reports the current src provenance status and what the build commands would do. - Use `tclsh src/make.tcl vfscommonupdate` to rebuild `_vfscommon.vfs`. - Uncommitted `make.tcl`-generated outputs may be batched into one commit, regardless of how many `make.tcl` invocations produced them. This covers punkcheck-managed build outputs that are VCS-tracked: `src/bootsupport/`, `_vfscommon.vfs/modules` + `/lib`, and declared per-kit `*.vfs/lib_tcl/` subfolders. (Root `modules/`, `lib/`, `modules_tcl/`, `lib_tcl/` are gitignored and never committed.) Each module's old→new version rename is atomic; a regeneration reflects one build-output refresh, not independent hand-edits. This is a carve-out from generic "split aggressively" commit guidance for punkcheck-managed build outputs only — it does not cover user-curated VFS content (`_config/`, per-kit `*.vfs/` outside declared `lib_tcl` subfolders, `_aside/`, `mkzipfix.vfs`, `_vfscommon.vfs/doc`), which commits separately per its own concerns. - Use `punk make.tcl project` or `punk902z make.tcl project` inside Punk shell when building binaries through Punk. diff --git a/src/make.tcl b/src/make.tcl index eead4196..1a7dc687 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -20,7 +20,7 @@ namespace eval ::punkboot { variable scriptfolder [file normalize [file dirname [info script]]] variable foldername [file tail $scriptfolder] variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] - variable non_help_flags [list -k] + variable non_help_flags [list -k -dirty-abort] variable help_flags [list -help --help /? -h] variable known_commands [list project modules libs packages vfs vfslibs bin info check shell vendorupdate bootsupport vfscommonupdate projectversion] } @@ -1364,6 +1364,13 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - run the punk shell using bootsupport libraries." \n append h " $scriptname projectversion" \n append h " - advisory check: verify CHANGELOG.md matches punkproject.toml and warn if src/ has changes since the last project-version bump." \n \n + append h " Flags:" \n + append h " -dirty-abort" \n + append h " - abort build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) when src/ has" \n + append h " uncommitted VCS changes. Default is warn-only: artifacts built from dirty src have no committed provenance." \n + append h " Warnings carry a plain PROVENANCE-WARNING: prefix (greppable in redirected output) and are recapped at the end of the run." \n + append h " Use '$scriptname check' to see the current provenance status. To evaluate uncommitted source without building," \n + append h " use ' src' or ' src shell'." \n \n append h "" \n if {[llength [dict get $pkg_availability missing]] || [llength [dict get $pkg_availability broken]]} { set has_recommended 0 @@ -1604,6 +1611,98 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} set sourcefolder $projectroot/src set binfolder $projectroot/bin +# ---------------------------------------- +# Provenance-warning presentation + dirty-src check for build/promotion commands (goal G-026 direction). +# Build/promotion commands stamp versions onto src content and propagate it into trees other +# things consume (root modules/, bootsupport snapshots, vfs payloads, kits/zipkits). +# Artifacts built from uncommitted src have no committed provenance - warn by default, +# abort if the -dirty-abort flag was given. To evaluate uncommitted source without +# building, use ' src' / ' src shell' instead. +# Guarded require: a stale or missing punkboot::utils bootsupport snapshot must never +# brick the make.tcl commands used to repair it - the check degrades to a skip notice +# (but -dirty-abort still aborts rather than silently skipping the requested strictness). + +#Emit provenance warnings with a plain column-0 token for automated discovery (grep PROVENANCE-WARNING +#in redirected output) and ANSI colour for humans. Lines are also accumulated in +#::punkboot::provenance_warnings_pending unless -norecap is given, so the wrapped ::exit below can +#recap them at the tail of the output - build progress output is chatty and the original warning +#position may scroll out of sight or even out of scrollback. +set ::punkboot::provenance_warnings_pending [list] +proc ::punkboot::print_provenance_warnings {warninglines args} { + global A + if {![array size A]} {punkboot::define_global_ansi} + foreach w $warninglines { + regsub {^WARNING: } $w "" w + puts stderr "PROVENANCE-WARNING: $A(BAD)$w$A(RST)" + if {"-norecap" ni $args} { + lappend ::punkboot::provenance_warnings_pending $w + } + } + flush stderr +} +#Availability probe + scoped dirty-src check, shared by the build/promotion gate below and the +#'check' command report. Returns {available warninglist}. +proc ::punkboot::get_src_provenance_warnings {projectroot label} { + set available [expr {\ + ![catch {package require punkboot::utils}]\ + && [llength [info commands ::punkboot::utils::vcs_dirty_warnings]]\ + && "scope" in [info args ::punkboot::utils::vcs_dirty_warnings]\ + }] + if {!$available} { + return [list 0 [list]] + } + set checked_vcs_roots [dict create] + return [list 1 [::punkboot::utils::vcs_dirty_warnings $projectroot checked_vcs_roots $label src]] +} +#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. +if {![llength [info commands ::punkboot::exit_original]]} { + rename ::exit ::punkboot::exit_original + proc ::exit {{returnCode 0}} { + if {[info exists ::punkboot::provenance_warnings_pending] && [llength $::punkboot::provenance_warnings_pending]} { + puts stderr "" + puts stderr "PROVENANCE-WARNING: recap of warning(s) emitted earlier in this run:" + ::punkboot::print_provenance_warnings $::punkboot::provenance_warnings_pending -norecap + } + ::punkboot::exit_original $returnCode + } +} + +if {$::punkboot::command in {project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate}} { + set dirty_abort [expr {[lsearch $::argv -dirty-abort] >= 0}] + lassign [::punkboot::get_src_provenance_warnings $projectroot "make.tcl $::punkboot::command"] have_scoped_dirty_check dirty_warnings + if {$have_scoped_dirty_check} { + if {[llength $dirty_warnings]} { + ::punkboot::print_provenance_warnings $dirty_warnings + puts stderr " To evaluate uncommitted source without building, use ' src' or ' src shell'." + if {$dirty_abort} { + set ::punkboot::provenance_warnings_pending [list] ;#no recap needed - aborting adjacent to the warning + puts stderr "-aborted- (-dirty-abort given and src has uncommitted changes - commit first, or rerun without -dirty-abort to build with a warning)" + exit 1 + } + #Grace period for an attentive human to ctrl-c - only when stdin is a terminal + #(-inputmode is only supported on terminal channels, Tcl 8.7+/9; pipes/redirects/8.6 skip the delay + # so agent/CI runs pay nothing) + if {![catch {chan configure stdin -inputmode}]} { + puts -nonewline stderr " proceeding despite dirty src - ctrl-c now to abort " + flush stderr + foreach tick {3 2 1} { + puts -nonewline stderr "..$tick" + flush stderr + after 1000 + } + puts stderr "" + } + } + } else { + if {$dirty_abort} { + puts stderr "-aborted- (-dirty-abort given but the dirty-src provenance check is unavailable: punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport)" + exit 1 + } + puts stderr "NOTE: dirty-src provenance check unavailable (punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport) - continuing without it" + } +} + if {$::punkboot::command eq "check"} { set sep [string repeat - 75] puts stdout $sep @@ -1738,6 +1837,21 @@ if {$::punkboot::command eq "check"} { puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport" puts stderr "==============================================================================" } + # Dirty-src provenance status - what the build/promotion commands would do + puts stdout $sep + lassign [::punkboot::get_src_provenance_warnings $projectroot "make.tcl check"] _prov_available _prov_warnings + if {!$_prov_available} { + puts stdout "src provenance: check unavailable (punkboot::utils vcs_dirty_warnings with scope support not loadable from bootsupport)" + puts stdout " build/promotion commands will proceed with a NOTE; -dirty-abort would abort as unverifiable." + } elseif {![llength $_prov_warnings]} { + puts stdout "src provenance: OK (no uncommitted fossil/git changes under src/)" + puts stdout " build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) will proceed without provenance warnings." + } else { + ::punkboot::print_provenance_warnings $_prov_warnings -norecap + puts stdout " build/promotion commands (project packages modules libs vfs vfslibs bin bootsupport vfscommonupdate) will WARN as above and proceed." + puts stdout " With -dirty-abort they would abort. To evaluate uncommitted source without building, use ' src' or ' src shell'." + } + puts stdout $sep exit 0 } @@ -2040,9 +2154,7 @@ if {$::punkboot::command eq "vendorupdate"} { set normsrclocation [file normalize $srclocation] if {![dict exists $checked_source_paths $normsrclocation]} { dict set checked_source_paths $normsrclocation 1 - foreach w [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] { - puts stderr $w - } + ::punkboot::print_provenance_warnings [::punkboot::utils::vcs_dirty_warnings $normsrclocation checked_vcs_roots vendorupdate] } } #puts stdout "$relpath $module $module_subpath $srclocation" diff --git a/src/modules/punkboot/utils-999999.0a1.0.tm b/src/modules/punkboot/utils-999999.0a1.0.tm index b65abd3e..1fdbb2a3 100644 --- a/src/modules/punkboot/utils-999999.0a1.0.tm +++ b/src/modules/punkboot/utils-999999.0a1.0.tm @@ -134,6 +134,10 @@ namespace eval punkboot::utils { 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." @@ -144,9 +148,11 @@ namespace eval punkboot::utils { "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 ""}} { + 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 "} @@ -155,6 +161,12 @@ namespace eval punkboot::utils { 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} { @@ -171,8 +183,8 @@ namespace eval punkboot::utils { if {$parent eq $dir} { break } set dir $parent } - if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} { - dict set checkedroots fossil,$fossilroot 1 + 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 { @@ -180,19 +192,37 @@ namespace eval punkboot::utils { 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)" - } elseif {$fchanges ne ""} { - lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" + } 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 ' ' + 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] && [llength [auto_execok git]]} { - dict set checkedroots git,$gitroot 1 + 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]] + 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 ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" + 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 diff --git a/src/modules/punkboot/utils-buildversion.txt b/src/modules/punkboot/utils-buildversion.txt index 98bd2835..59fc8f57 100644 --- a/src/modules/punkboot/utils-buildversion.txt +++ b/src/modules/punkboot/utils-buildversion.txt @@ -1,5 +1,6 @@ -0.1.1 +0.2.0 #First line must be a semantic version number #all other lines are ignored. +#0.2.0 - vcs_dirty_warnings: optional scope arg limiting which uncommitted changes count (e.g scope src for make.tcl build/promotion dirty-src gate) #0.1.1 - added vcs_dirty_warnings (dirty fossil/git checkout provenance warnings; used by make.tcl vendorupdate, shared with bootsupport update per goal G-026) #0.1.0 - initial: parse_punkproject_version, read_punkproject_version, read_changelog_latest_version extracted from make.tcl inline procs