Browse Source

make.tcl bootstrapping fix, punkcheck refactor and tests

master
Julian Noble 2 weeks ago
parent
commit
df78ff9bb5
  1. 2
      scriptlib/exit.tcl
  2. 1
      src/AGENTS.md
  3. 25
      src/bootsupport/AGENTS.md
  4. 7
      src/bootsupport/modules/punk/ansi-0.1.1.tm
  5. 14
      src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  6. 1427
      src/bootsupport/modules/punk/mix/cli-0.4.0.tm
  7. 324
      src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm
  8. 1178
      src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm
  9. 1178
      src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm
  10. 70
      src/bootsupport/modules/punk/path-0.1.0.tm
  11. 1611
      src/bootsupport/modules/punk/path-0.2.0.tm
  12. 2479
      src/bootsupport/modules/punkcheck-0.2.0.tm
  13. 178
      src/make.tcl
  14. 17
      src/modules/AGENTS.md
  15. 1
      src/modules/punk/AGENTS.md
  16. 7
      src/modules/punk/ansi-999999.0a1.0.tm
  17. 14
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/layouts/project/src/make.tcl
  18. 14
      src/modules/punk/mix/cli-999999.0a1.0.tm
  19. 3
      src/modules/punk/mix/cli-buildversion.txt
  20. 2
      src/modules/punk/mix/commandset/doc-999999.0a1.0.tm
  21. 3
      src/modules/punk/mix/commandset/doc-buildversion.txt
  22. 23
      src/modules/punk/mix/commandset/project-999999.0a1.0.tm
  23. 4
      src/modules/punk/mix/commandset/project-buildversion.txt
  24. 91
      src/modules/punk/path-999999.0a1.0.tm
  25. 4
      src/modules/punk/path-buildversion.txt
  26. 333
      src/modules/punkcheck-999999.0a1.0.tm
  27. 6
      src/modules/punkcheck-buildversion.txt
  28. 178
      src/project_layouts/custom/_project/punk.basic/src/make.tcl
  29. 7
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm
  30. 14
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  31. 1427
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.4.0.tm
  32. 324
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm
  33. 1178
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm
  34. 1178
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm
  35. 70
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm
  36. 1611
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.2.0.tm
  37. 2479
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.2.0.tm
  38. 178
      src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl
  39. 7
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm
  40. 14
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  41. 1427
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.4.0.tm
  42. 324
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm
  43. 1178
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm
  44. 1178
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm
  45. 70
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm
  46. 1611
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.2.0.tm
  47. 2479
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.2.0.tm
  48. 178
      src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl
  49. 2
      src/tests/modules/AGENTS.md
  50. 94
      src/tests/modules/punk/path/testsuites/tests/path.test
  51. 684
      src/tests/modules/punkcheck/testsuites/punkcheck/install.test
  52. 613
      src/tests/modules/punkcheck/testsuites/punkcheck/installtrack.test
  53. 1
      src/tests/runtests.tcl
  54. 518
      src/vfs/_vfscommon.vfs/modules/commandstack-0.3.tm
  55. 201
      src/vfs/_vfscommon.vfs/modules/oolib-0.1.2.tm
  56. 7
      src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm
  57. 14
      src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm
  58. 1427
      src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.4.0.tm
  59. 324
      src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/doc-0.1.1.tm
  60. 1178
      src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.1.tm
  61. 1178
      src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.2.0.tm
  62. BIN
      src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.1.3.tm
  63. 70
      src/vfs/_vfscommon.vfs/modules/punk/path-0.1.0.tm
  64. 1611
      src/vfs/_vfscommon.vfs/modules/punk/path-0.2.0.tm
  65. 2479
      src/vfs/_vfscommon.vfs/modules/punkcheck-0.2.0.tm

2
scriptlib/exit.tcl

@ -0,0 +1,2 @@
#call exit with whatever we pass in.
exit {*}$::argv

1
src/AGENTS.md

@ -72,6 +72,7 @@ Recovery after a wrong path guess:
- Review VS Code Tcl lint diagnostics before submitting new agent-produced Tcl code, but do not use lint tooling to reformat existing code.
- When touching VFS payloads, describe regeneration steps in durable docs if the workflow changes.
- When a tool summary reports cleanup issues such as `git diff --check` whitespace errors, verify against exact command output and exit code before running secondary scans or reporting the issue.
- `make.tcl` performs version-aware bootsupport staleness detection: major bumps abort, minor bumps prompt (y/N), patch bumps warn-and-proceed. See `src/bootsupport/AGENTS.md` "Bootsupport Staleness Handling" for the full contract and the version-bump discipline agents must follow.
## Verification

25
src/bootsupport/AGENTS.md

@ -17,6 +17,31 @@ Modules and libraries required during the build/bootstrap/make process before th
- The `include_modules.config` file controls which vendormodules are pulled into bootsupport.
- Bootsupport snapshots do not use the `999999.0a1.0` source-module magic version unless explicitly copied from a source module with the prescribed workflow.
## Bootsupport Staleness Handling
`src/make.tcl` compares each tracked bootsupport module's highest `.tm` version against the corresponding `src/modules/<...>-buildversion.txt` for these runtime-critical packages: `punkcheck`, `punk::repo`, `punk::mix`, `punk::tdl`. When the source version is higher than the bootsupport version, `make.tcl` classifies the bump level from the version delta and reacts as follows (except for the `check` command, which only reports staleness at the end of its output):
- **Major bump** (e.g 1.x -> 2.x): abort unconditionally. Breaking change means the old bootsupport snapshot will not work.
- **Minor bump** (e.g 0.1 -> 0.2): prompt the user (y/N, default No) whether to proceed. Backward-compat aliases *should* keep old bootsupport functional, but the user must opt in.
- **Patch bump** (e.g 0.1.1 -> 0.1.2): warn and proceed. No prompt.
The prompt is self-contained Tcl (modeled on `punk::lib::askuser` via `::punkboot::lib::bootsupport_prompt_yesno`) because it runs in the early boot phase before punk packages are guaranteed loaded. The classifier is `::punkboot::lib::bootsupport_bump_level`.
### Version-Bump Discipline (agent-relevant)
Because the staleness classifier keys off the version delta, under-bumping a module (e.g recording a patch bump when the call-site API actually changed) will cause `make.tcl` to mis-classify the bump and either wrongly proceed without prompting or wrongly abort. This discipline applies specifically to the four files the classifier reads — `punkcheck-buildversion.txt`, `punk/repo-buildversion.txt`, `punk/mix-buildversion.txt` (top-level `punk::mix` only, not its submodules), and `punk/tdl-buildversion.txt`. Follow the rules in `src/modules/AGENTS.md` "Versioning And Releases" exactly:
- Call-site API change (new flags, renamed flags, changed call shapes) -> at least a **minor** bump, even if the module's own interface is unchanged.
- Backward-compatible API additions (new procs, new flags with defaults) -> **minor**.
- Bug fixes, internal refactors, docs-only updates -> **patch**.
- Breaking API changes (removed flags, changed return shapes, renamed procs without aliases) -> **major**.
For modules outside this tracked set (including `punk::mix::util`, `punk::mix::cli`, and other `punk::mix` submodules), ordinary SemVer judgement applies instead — see `src/modules/AGENTS.md` "Versioning And Releases". `make.tcl` does not read their versions, so a behavior-preserving call-site change there is a patch, not a forced minor.
If the set of runtime-critical packages in `make.tcl`'s `_runtime_deps` list ever grows, update this list (and the matching one in `src/modules/AGENTS.md`) so the two stay in sync.
After changing any build-critical module (`punkcheck`, `punk::repo`, `punk::mix`, `punk::tdl`), rebuild bootsupport with `cd src && tclsh make.tcl modules && tclsh make.tcl bootsupport` so the snapshot matches the source version and staleness does not trip.
## Work Guidance
- Verify boot functionality by running `tclsh src/make.tcl packages` after any change.

7
src/bootsupport/modules/punk/ansi-0.1.1.tm

@ -1325,6 +1325,13 @@ tcl::namespace::eval punk::ansi {
grepstr -r m -C 2 {cancelid} [s ::after] ;#with 2 lines of context and no break indicators.
}]}
To match a literal string that contains regex special characters, it is in some cases easier to use the Tcl regex
quoting switch (?q) to quote the string rather than escaping each special character with a backslash.
${[punk::args::helpers::example -syntax none {
#The following example matches a literal string that is the Tcl expansion operator {*}
grepstr "(?q){*}" $sometext
}]}
}
@leaders -min 0 -max 0

14
src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -474,7 +474,7 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-punkcheck_eventobj "\uFFFF"
-glob *.tm
@ -487,7 +487,7 @@ namespace eval punk::mix::cli {
set installername [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set CALLDEPTH [dict get $opts -call-depth-internal]
set max_depth [dict get $opts -max_depth]
set max_depth [dict get $opts -max-depth]
set subdirlist [dict get $opts -subdirlist]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set fileglob [dict get $opts -glob]
@ -533,7 +533,7 @@ namespace eval punk::mix::cli {
set config [dict create {*}{
-glob $fileglob
-max_depth 0
-max-depth 0
}]
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
@ -628,11 +628,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
@ -829,11 +829,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder

1427
src/bootsupport/modules/punk/mix/cli-0.4.0.tm

File diff suppressed because it is too large Load Diff

324
src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm

@ -0,0 +1,324 @@
# -*- 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 punk::mix::commandset::doc 0.1.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require punk::path ;# for treefilenames, relative
package require punk::repo
package require punk::docgen ;#inline doctools - generate doctools .man files at src/docgen prior to using kettle to producing .html .md etc
package require punk::mix::cli ;#punk::mix::cli::lib used for kettle_call
#package require punkcheck ;#for path_relative
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::mix::commandset::doc {
namespace export *
proc _default {} {
puts "documentation subsystem"
puts "commands: doc.build"
puts " build documentation from src/doc to src/embedded using the kettle build tool"
puts "commands: doc.status"
}
proc build {} {
puts "build docs"
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to build docs"
return
}
#user may delete the comment containing "--- punk::docgen::overwrites" and then manually edit, and we won't overwrite
#we still generate output in src/docgen so user can diff and manually update if thats what they prefer
set oldfiles [punk::path::treefilenames -dir $projectdir/src/doc _module_*.man]
foreach maybedoomed $oldfiles {
set fd [open $maybedoomed r]
chan conf $fd -translation binary
set data [read $fd]
close $fd
if {[string match "*--- punk::docgen overwrites *" $data]} {
file delete -force $maybedoomed
}
}
set generated [lib::do_docgen modules]
if {[dict get $generated count] > 0} {
#review
set doclist [dict get $generated docs]
set source_base [dict get $generated base]
set target_base $projectdir/src/doc
foreach dinfo $doclist {
lassign $dinfo module fpath
set relpath [punk::path::relative $source_base $fpath]
set relfolder [file dirname $relpath]
if {$relfolder eq "."} {
set relfolder ""
}
file mkdir [file join $target_base $relfolder]
set target [file join $target_base $relfolder _module_[file tail $fpath]]
puts stderr "target --> $target"
if {![file exists $target]} {
file copy $fpath $target
}
}
}
if {[file exists $projectdir/src/doc]} {
set original_wd [pwd]
cd $projectdir/src
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init VIRTUAL kettle_build_doc ;#VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
if {\
[llength [dict get [$event targetset_source_changes] changed]]\
} {
$event targetset_started
# -- --- --- --- --- ---
puts stdout "BUILDING DOCS at $projectdir/src/embedded from src/doc"
if {[catch {
if {"::meta" eq [info commands ::meta]} {
puts stderr "There appears to be a leftover ::meta command which is presumed to be from doctools. Destroying object"
::meta destroy
}
punk::mix::cli::lib::kettle_call lib doc
#Kettle doc
} errM]} {
$event targetset_end FAILED -note "kettle_build_doc failed: $errM"
} else {
$event targetset_end OK
}
# -- --- --- --- --- ---
} else {
puts stderr "No change detected in src/doc"
$event targetset_end SKIPPED
}
$event end
$event destroy
$installer destroy
cd $original_wd
} else {
puts stderr "No doc folder found at $projectdir/src/doc"
}
}
proc status {} {
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
cd $projectdir/src
puts stdout "Testing status of doctools source location $projectdir/src/doc ..."
flush stdout
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init QUERY kettle_build_doc ;#usually VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated - but here we use QUERY to ensure no writes to .punkcheck
set last_completion [$event targetset_last_complete]
if {[llength $last_completion]} {
#adding a source causes it to be checksummed
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
set changeinfo [$event targetset_source_changes]
if {\
[llength [dict get $changeinfo changed]]\
} {
puts stdout "changed"
puts stdout $changeinfo
} else {
puts stdout "No changes detected in $projectdir/src/doc tree"
}
} else {
#no previous completion-record for this target - must assume changed - no need to trigger checksumming
puts stdout "No existing record of doc build in .punkcheck. Assume it needs to be rebuilt."
}
$event destroy
$installer destroy
cd $original_wd
}
proc validate {args} {
set argd [punk::args::parse $args withdef {
@id -id ::punk::mix::commandset::doc::validate
-- -type none -optional 1 -help "end of options marker --"
-individual -type boolean -default 1
@values -min 0 -max -1
patterns -default {*.man} -type any -multiple 1
}]
set opt_individual [tcl::dict::get $argd opts -individual]
set patterns [tcl::dict::get $argd values patterns]
#todo - run and validate punk::docgen output
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
set docroot $projectdir/src/doc
cd $docroot
if {!$opt_individual && "*.man" in $patterns} {
if {[catch {
dtplite validate $docroot
} errM]} {
puts stderr "commandset::doc::validate failed for projectdir '$projectdir'"
puts stderr "docroot '$docroot'"
puts stderr "dtplite error was: $errM"
}
} else {
foreach p $patterns {
set treefiles [punk::path::treefilenames $p]
foreach path $treefiles {
puts stdout "dtplite validate $path"
dtplite validate $path
}
}
}
#punk::mix::cli::lib::kettle_call lib validate-doc
cd $original_wd
}
namespace eval collection {
variable pkg
set pkg punk::mix::commandset::doc
namespace export *
namespace path [namespace parent]
}
namespace eval lib {
variable pkg
set pkg punk::mix::commandset::doc
proc do_docgen {{project_subpath modules}} {
#Extract doctools comments from source code
set projectdir [punk::repo::find_project]
set output_base [file join $projectdir src docgen]
set codesource_path [file join $projectdir $project_subpath]
if {![file isdirectory $codesource_path]} {
puts stderr "WARNING punk::mix::commandset::doc unable to find codesource_path $codesource_path during do_docgen - skipping inline doctools generation"
return
}
if {[file isdirectory $output_base]} {
if {[catch {
file delete -force $output_base
}]} {
error "do_docgen failed to delete existing output base folder: $output_base"
}
}
file mkdir $output_base
set matched_paths [punk::path::treefilenames -dir $codesource_path -exclude-paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set count 0
set newdocs [list]
set docgen_header_comments ""
append docgen_header_comments {[comment {--- punk::docgen generated from inline doctools comments ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen DO NOT EDIT DOCS HERE UNLESS YOU REMOVE THESE COMMENT LINES ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen overwrites this file ---}]} \n
foreach fullpath $matched_paths {
puts stdout "do_docgen processing: $fullpath"
set doctools [punk::docgen::get_doctools_comments $fullpath]
if {$doctools ne ""} {
set fname [file tail $fullpath]
set mod_tail [file rootname $fname]
set relpath [punk::path::relative $codesource_path [file dirname $fullpath]]
if {$relpath eq "."} {
set relpath ""
}
set tailsegs [file split $relpath]
set module_fullname [join $tailsegs ::]::$mod_tail
set target_docname $fname.man
set this_outdir [file join $output_base $relpath]
if {[string length $fname] > 99} {
#output needs to be tarballed to do checksum change tests in a reasonably straightforward and not-too-terribly slow way.
#hack - review. Determine exact limit - test if tcllib tar fixed or if it's a limit of the particular tar format
#work around tcllib tar filename length limit ( somewhere around 100?) This seems to be a limit on the length of a particular segment in the path.. not whole path length?
#this case only came up because docgen used to path munge to long filenames - but left because we know there is a limit and renaming fixes it - even if it's ugly - but still allows doc generation.
#review - if we're checking fname - should also test length of whole path and determine limits for tar
package require md5
if {[package vsatisfies [package present md5] 2- ] } {
set md5opt "-hex"
} else {
set md5opt ""
}
set target_docname [md5::md5 {*}$md5opt [encoding convertto utf-8 $fullpath]]_overlongfilename.man
puts stderr "WARNING - overlong file name - renaming $fullpath"
puts stderr " to [file dirname $fullpath]/$target_docname"
}
file mkdir $this_outdir
puts stdout "saving [string length $doctools] bytes of doctools output from file $relpath/$fname"
set outfile [file join $this_outdir $target_docname]
set fd [open $outfile w]
fconfigure $fd -translation binary
puts -nonewline $fd $docgen_header_comments$doctools
close $fd
incr count
lappend newdocs [list $module_fullname $outfile]
}
}
return [list count $count docs $newdocs base $output_base]
}
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::mix::commandset::doc [namespace eval punk::mix::commandset::doc {
variable pkg punk::mix::commandset::doc
variable version
set version 0.1.1
}]
return

1178
src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm

File diff suppressed because it is too large Load Diff

1178
src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm

File diff suppressed because it is too large Load Diff

70
src/bootsupport/modules/punk/path-0.1.0.tm

@ -1444,52 +1444,34 @@ namespace eval punk::path {
return [treefile_priv::walk_treefilenames_zipfs $state]
}
punk::args::define {
@id -id ::punk::path::relative
@cmd -name punk::path::relative\
-summary\
"Compute the relative path from a reference path to a location path."\
-help\
"Taking two directory paths, a reference and a location, computes the path
of the location relative to the reference.
Will return a single dot '.' if the paths are the same.
Both paths must be the same type - ie both absolute or both relative.
Matching is case sensitive. On windows, the drive-letter component (only) is
not case sensitive, so punk::path::relative c:/etc C:/etc returns '.'.
The part following the driveletter is case sensitive, so
punk::path::relative c:/etc C:/Etc returns ../Etc.
On windows, if the paths are absolute and specify different volumes,
only the location will be returned."
@leaders -min 2 -max 2
reference -type string -help\
"The path from which the relative path to location is determined."
location -type string -help\
"The location path which may be above or below the reference path."
}
#maint warning - also in punkcheck
proc relative {reference location} {
#*** !doctools
#[call [fun relative] [arg reference] [arg location]]
#[para] Taking two directory paths, a reference and a location, computes the path
# of the location relative to the reference.
#[list_begin itemized]
#[item]
#[para] Arguments:
# [list_begin arguments]
# [arg_def string reference] The path from which the relative path to location is determined.
# [arg_def string location] The location path which may be above or below the reference path
# [list_end]
#[item]
#[para] Results:
#[para] The relative path of the location to the reference path.
#[para] Will return a single dot "." if the paths are the same
#[item]
#[para] Notes:
#[para] Both paths must be the same type - ie both absolute or both relative
#[para] Case sensitive. ie punk::path::relative /etc /etC
# will return ../etC
#[para] On windows, the drive-letter component (only) is not case sensitive
#[example_begin]
# P% punk::path::relative c:/etc C:/etc
# - .
#[example_end]
#[para] The part following the driveletter is case sensitive so in the following cases it recognises the driveletter matches but not the tail
#[example_begin]
# P% punk::path::relative c:/etc C:/Etc
# - ../Etc
#[example_end]
#[para] On windows, if the paths are absolute and specifiy different volumes, only the location will be returned.
#[example_begin]
# P% punk::path::relative c:/etc d:/etc/blah
# - d:/etc/blah
#[example_end]
#[para] Unix-like examples:
#[example_begin]
# P% punk::path::relative /usr/local/etc/ /usr/local/etc/somewhere/below
# - somewhere/below
# P% punk::path::relative /usr/local/etc/somewhere /usr/local/lib/here
# - ../../lib/here
#[example_end]
#[list_end]
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).

1611
src/bootsupport/modules/punk/path-0.2.0.tm

File diff suppressed because it is too large Load Diff

2479
src/bootsupport/modules/punkcheck-0.2.0.tm

File diff suppressed because it is too large Load Diff

178
src/make.tcl

@ -282,6 +282,52 @@ namespace eval ::punkboot::lib {
}
#------------------------------------------------------------------------------
# Bootsupport staleness helpers self-contained Tcl, no punk package deps.
# These run in the early boot phase before punk packages are guaranteed loaded
# (modeled on punk::lib::askuser but without punk::console / raw-mode deps).
#------------------------------------------------------------------------------
# Classify the version delta between an old (bootsupport) and new (source)
# semver as "major", "minor", or "patch".
# Caller must verify new_ver > old_ver via `package vcompare` first.
proc ::punkboot::lib::bootsupport_bump_level {old_ver new_ver} {
set op [split $old_ver .]
set np [split $new_ver .]
while {[llength $op] < 3} {lappend op 0}
while {[llength $np] < 3} {lappend np 0}
set op [lmap s $op {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
set np [lmap s $np {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
lassign $op o_major o_minor o_patch
lassign $np n_major n_minor n_patch
if {$n_major > $o_major} {return "major"}
if {$n_minor > $o_minor} {return "minor"}
return "patch"
}
# Self-contained y/N prompt. Returns 1 for y/yes (case-insensitive), 0 otherwise.
# Used by the staleness abort path to ask the user whether to proceed past a
# minor-version bump. Default is No (Enter = abort) so accidental Enter is safe.
proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
puts stdout $prompt
puts -nonewline stdout "Proceed anyway? (y/N) "
flush stdout
set stdin_state [chan configure stdin]
try {
chan configure stdin -blocking 1
set answer [string tolower [string trim [gets stdin]]]
} finally {
chan configure stdin -blocking [dict get $stdin_state -blocking]
}
return [expr {$answer eq "y" || $answer eq "yes"}]
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -469,6 +515,52 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} {
#package forget punk::repo
#package forget punkcheck
#------------------------------------------------------------------
# Bootsupport staleness check
# When make.tcl is run from project root (not from src/), bootsupport
# modules are loaded in preference to source modules. If the source
# modules have been updated (e.g API changes) but bootsupport hasn't
# been rebuilt, make.tcl will fail with obscure errors from the stale
# bootsupport versions. This check compares buildversion.txt in source
# against the highest .tm version in bootsupport and warns the caller.
#------------------------------------------------------------------
if {[file tail $startdir] ne "src"} {
set _runtime_deps [list {*}{
} punkcheck punkcheck-buildversion.txt punkcheck-*.tm {*}{
} punk::repo punk/repo-buildversion.txt punk/repo-*.tm {*}{
} punk::mix punk/mix-buildversion.txt punk/mix-*.tm {*}{
} punk::tdl punk/tdl-buildversion.txt punk/tdl-*.tm {*}{
}]
set _stale_modules [list]
foreach {pkg buildversion_rel bootsupport_glob} $_runtime_deps {
set _src_bv_file [file join $::punkboot::scriptfolder modules $buildversion_rel]
set _bs_glob_dir [file dirname [file join $::punkboot::scriptfolder bootsupport modules $bootsupport_glob]]
set _bs_glob_pat [file tail $bootsupport_glob]
if {![file exists $_src_bv_file]} {continue}
#read source version (first line of buildversion.txt)
set _fd [open $_src_bv_file r]
set _src_version [string trim [lindex [split [read $_fd] \n] 0]]
close $_fd
#find highest version .tm in bootsupport
set _bs_version ""
set _prefix [string range $_bs_glob_pat 0 [string first "*" $_bs_glob_pat]-1]
foreach _tm [glob -nocomplain -dir $_bs_glob_dir -- $_bs_glob_pat] {
set _v [file rootname [string range [file tail $_tm] [string length $_prefix] end]]
if {$_bs_version eq "" || [package vcompare $_v $_bs_version] == 1} {
set _bs_version $_v
}
}
if {$_bs_version ne "" && $_src_version ne "" && [package vcompare $_src_version $_bs_version] == 1} {
set _bump [::punkboot::lib::bootsupport_bump_level $_bs_version $_src_version]
lappend _stale_modules [list $pkg $_src_version $_bs_version $_bump]
}
}
if {[llength $_stale_modules]} {
set ::punkboot::stale_bootsupport $_stale_modules
}
unset _runtime_deps _stale_modules
}
package require punk::repo ;#todo - push our requirements to a smaller punk::repo::xxx package with minimal dependencies
package require punk::mix
package require punkcheck
@ -1433,6 +1525,51 @@ if {[lsearch $::argv -k] >= 0} {
#puts stdout "::argv $::argv"
# ----------------------------------------
# Abort / prompt if bootsupport modules are stale relative to source.
# Behaviour is version-aware based on the bump level between each stale
# module's bootsupport version and source version:
# - major bump -> abort unconditionally (breaking change, old bootsupport will not work)
# - minor bump -> prompt user; backward-compat aliases *should* keep old bootsupport functional
# - patch bump -> warn and proceed (non-breaking)
# 'check' is exempt in all cases the warning is shown at the end of its output instead.
if {[info exists ::punkboot::stale_bootsupport] && $::punkboot::command ne "check"} {
set _stale $::punkboot::stale_bootsupport
set _have_major 0
set _have_minor 0
set _have_patch 0
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
switch -- $_bump {
major {incr _have_major}
minor {incr _have_minor}
patch {incr _have_patch}
default {incr _have_patch}
}
}
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Recommended fix update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
if {$_have_major} {
puts stderr "-aborted- (major version bump in stale bootsupport breaking change, cannot proceed)"
exit 1
}
if {$_have_minor} {
if {![::punkboot::lib::bootsupport_prompt_yesno "Minor-version staleness backward-compat aliases should keep old bootsupport functional."]} {
puts stderr "-aborted- by user"
exit 1
}
puts stderr "-proceeding with stale bootsupport at user's request-"
}
# patch-only staleness: warn and proceed (warning already printed above)
}
set scriptfolder $::punkboot::scriptfolder
@ -1582,6 +1719,19 @@ if {$::punkboot::command eq "check"} {
puts stdout $sep
catch {package require struct::set}
puts stdout ---[package ifneeded struct::set 2.2.3]
# Show bootsupport staleness warning at the end of check output
if {[info exists ::punkboot::stale_bootsupport]} {
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $::punkboot::stale_bootsupport {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Run the following to update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
}
exit 0
}
@ -2025,7 +2175,7 @@ if {$::punkboot::command eq "bootsupport"} {
set resultdict [punkcheck::install $sourcemodules $targetroot\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-installer "punkboot-bootsupport"\
-progresschannel stdout\
]
@ -2042,7 +2192,7 @@ if {$::punkboot::command eq "bootsupport"} {
puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $source_bootscript to $targetroot_bootscript (if source file changed)"
set resultdict [punkcheck::install [file dirname $source_bootscript] $targetroot_bootscript\
-glob make.tcl\
-max_depth 1\
-max-depth 1\
-createempty 0\
-overwrite installedsourcechanged-targets\
-installer "punkboot-bootsupport"\
@ -2098,11 +2248,11 @@ if {$::punkboot::command in {project packages modules}} {
-overwrite installedsourcechanged-targets
-progresschannel stdout
-exclude-filetails {AGENTS.md include_modules.config}
-antiglob_paths {README.md}
-exclude-paths {README.md}
}]
# -antiglob_paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
# -exclude-paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
#-exclude-filetails {AGENTS.md include_modules.config}
#-antiglob_paths {README.md}
#-exclude-paths {README.md}
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}
if {![llength $vendormodulefolders]} {
@ -2112,7 +2262,7 @@ if {$::punkboot::command in {project packages modules}} {
if {$::punkboot::command in {project packages libs}} {
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2130,7 +2280,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB: copying tcl-version neutral and platform neutral libraries from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2150,7 +2300,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2165,7 +2315,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2183,7 +2333,7 @@ if {$::punkboot::command in {project packages libs}} {
########################################################
lappend projectlibfolders lib
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2200,7 +2350,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB: copying from $sourcefolder/$lf to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/$lf $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2219,7 +2369,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2235,7 +2385,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2358,7 +2508,7 @@ if {$::punkboot::command in {project packages modules}} {
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base\
-installer make.tcl\
-overwrite $overwrite\
-antiglob_paths {README.md **/AGENTS.md AGENTS.md}\
-exclude-paths {README.md **/AGENTS.md AGENTS.md}\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]

17
src/modules/AGENTS.md

@ -293,12 +293,25 @@ return
- Stick to semantic versioning: `major.minor.patch`.
- Use the magic version `999999.0a1.0` for source modules and add or update the actual version number in the associated `<modulename>-buildversion.txt` file.
- Update the `<modulename>-buildversion.txt` file when API changes are made.
- Bump `<modulename>-buildversion.txt` whenever the module's API or behavior changes:
- **Patch** (e.g 0.1.0 → 0.1.1): bug fixes, internal refactors with no API change, or documentation-only updates to PUNKARGS.
- **Minor** (e.g 0.1.0 → 0.2.0): new flags, procs, or backward-compatible API additions.
- **Major** (e.g 1.0.0 → 2.0.0): breaking API changes (removed flags, changed return shapes, renamed procs without aliases).
- When bumping, append changelog comment lines below the version documenting each change, prefixed with the new version number. e.g:
```
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#0.2.0 - merged -exclude-dirsegments + -antiglob_paths into unified -exclude-paths (backward-compat aliases retained)
#0.1.2 - fixed path_relative delegation edge case on Windows
```
This keeps the changelog discoverable alongside the version number without requiring a separate CHANGES file.
- **Bootstrap-tracked files only**`punkcheck-buildversion.txt`, `punk/repo-buildversion.txt`, `punk/mix-buildversion.txt` (the top-level `punk::mix` file only; `punk::mix::util`, `punk::mix::cli`, and `punk/mix/commandset/*` version independently and are not covered by this check), and `punk/tdl-buildversion.txt`: bump at least **minor** whenever a call site inside that file is updated to use a new API, even if the file's own interface is unchanged. `src/make.tcl` reads only these four files to classify bootsupport staleness (major=abort, minor=prompt, patch=silent-proceed); under-bumping one of them to patch when the call-site change is more significant causes `make.tcl` to mis-classify and either wrongly proceed without prompting or wrongly abort. See `src/bootsupport/AGENTS.md` "Bootsupport Staleness Handling" for the full contract.
- **All other modules** (including `punk::mix` submodules): call-site updates follow ordinary judgement from the Patch/Minor/Major rules above — a behavior-preserving call-site change is a patch (or no bump if genuinely a no-op); reserve minor for changes that add capability to the module's *own* API. `src/make.tcl` does not read these versions, so there is no automated consequence, but keep the changelog accurate since it is the only record of the module's real semantic version.
- There may be occasional exceptions such as `punk::libunknown-0.1.tm`, which is deliberately manually versioned; new modules should use the magic version mechanism.
- Modules with the magic version number must not appear in output paths such as `<projectdir>/modules`.
- When referencing ranges, use bounded specs such as `1.2.3-2.0.0`.
- Convert loose versions to bounded form in module metadata; helper utilities exist in boot modules for this purpose.
- Update `punk::libunknown` registries whenever adding or removing modules to keep discovery accurate.
### Documentation And Comments

1
src/modules/punk/AGENTS.md

@ -32,6 +32,7 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima
- Private helpers go in `namespace eval private { ... }` blocks.
- Keep `namespace export` lists alphabetized.
- The `#modpod-*` folders contain files that get packed into `.tm` module archives during build — do not flatten them.
- Follow the parent `src/modules/AGENTS.md` "Versioning And Releases" rules: bump `<modulename>-buildversion.txt` on any API or behavior change and append a changelog comment line documenting the change.
## Verification

7
src/modules/punk/ansi-999999.0a1.0.tm

@ -1325,6 +1325,13 @@ tcl::namespace::eval punk::ansi {
grepstr -r m -C 2 {cancelid} [s ::after] ;#with 2 lines of context and no break indicators.
}]}
To match a literal string that contains regex special characters, it is in some cases easier to use the Tcl regex
quoting switch (?q) to quote the string rather than escaping each special character with a backslash.
${[punk::args::helpers::example -syntax none {
#The following example matches a literal string that is the Tcl expansion operator {*}
grepstr "(?q){*}" $sometext
}]}
}
@leaders -min 0 -max 0

14
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/layouts/project/src/make.tcl

@ -169,7 +169,7 @@ if {[llength $commands_found] != 1 } {
set do_help 1
}
if {$do_help} {
puts stderr [punkmake_gethelp]
puts stderr [punkmake_gethelp]
exit 0
}
@ -443,7 +443,7 @@ if {$::punkmake::command eq "bootsupport"} {
puts stdout "BOOTSUPPORT non_tm_files $src - copying to $tgt (if source file changed)"
set overwrite "installedsourcechanged-targets"
set resultdict [punkcheck::install_non_tm_files $src $tgt -installer make.tcl -overwrite $overwrite -punkcheck_folder $projectroot/src/bootsupport]
set resultdict [punkcheck::install_non_tm_files $src $tgt -installer make.tcl -overwrite $overwrite -punkcheck-folder $projectroot/src/bootsupport]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}
}
@ -471,7 +471,7 @@ if {$::punkmake::command eq "bootsupport"} {
file mkdir $targetroot
puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $sourcemodules to $targetroot (if source file changed)"
set resultdict [punkcheck::install $sourcemodules $targetroot -overwrite installedsourcechanged-targets -antiglob_paths $antipaths]
set resultdict [punkcheck::install $sourcemodules $targetroot -overwrite installedsourcechanged-targets -exclude-paths $antipaths]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
flush stdout
}
@ -507,7 +507,7 @@ file mkdir $target_modules_base
#external libs and modules first - and any supporting files - no 'building' required
if {[file exists $sourcefolder/vendorlib]} {
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -516,7 +516,7 @@ if {[file exists $sourcefolder/vendorlib]} {
}]
puts stdout "VENDORLIB: copying from $sourcefolder/vendorlib to $projectroot/lib (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/vendorlib $projectroot/lib -overwrite installedsourcechanged-targets -antiglob_paths $antipaths]
set resultdict [punkcheck::install $sourcefolder/vendorlib $projectroot/lib -overwrite installedsourcechanged-targets -exclude-paths $antipaths]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
} else {
@ -526,7 +526,7 @@ if {[file exists $sourcefolder/vendorlib]} {
if {[file exists $sourcefolder/vendormodules]} {
#install .tm *and other files*
puts stdout "VENDORMODULES: copying from $sourcefolder/vendormodules to $target_modules_base (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md include_modules.config}]
set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -exclude-paths {README.md include_modules.config}]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
} else {
puts stderr "VENDORMODULES: No src/vendormodules folder found."
@ -621,7 +621,7 @@ foreach src_module_dir $source_module_folderlist {
set overwrite "installedsourcechanged-targets"
#set overwrite "ALL-TARGETS"
puts stdout "MODULEFOLDER non_tm_files $src_module_dir - copying to $target_modules_base (if source file changed)"
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base -installer make.tcl -overwrite $overwrite -antiglob_paths {README.md}]
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base -installer make.tcl -overwrite $overwrite -exclude-paths {README.md}]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}

14
src/modules/punk/mix/cli-999999.0a1.0.tm

@ -474,7 +474,7 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-punkcheck_eventobj "\uFFFF"
-glob *.tm
@ -487,7 +487,7 @@ namespace eval punk::mix::cli {
set installername [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set CALLDEPTH [dict get $opts -call-depth-internal]
set max_depth [dict get $opts -max_depth]
set max_depth [dict get $opts -max-depth]
set subdirlist [dict get $opts -subdirlist]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set fileglob [dict get $opts -glob]
@ -533,7 +533,7 @@ namespace eval punk::mix::cli {
set config [dict create {*}{
-glob $fileglob
-max_depth 0
-max-depth 0
}]
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
@ -628,11 +628,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
@ -829,11 +829,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder

3
src/modules/punk/mix/cli-buildversion.txt

@ -1,3 +1,4 @@
0.3.1
0.4.0
#First line must be a semantic version number
#all other lines are ignored.
#0.4.0 - updated -max_depth to -max-depth at punkcheck::install call sites — call-site flag change warrants minor bump

2
src/modules/punk/mix/commandset/doc-999999.0a1.0.tm

@ -245,7 +245,7 @@ namespace eval punk::mix::commandset::doc {
}
file mkdir $output_base
set matched_paths [punk::path::treefilenames -dir $codesource_path -antiglob_paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set matched_paths [punk::path::treefilenames -dir $codesource_path -exclude-paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set count 0
set newdocs [list]
set docgen_header_comments ""

3
src/modules/punk/mix/commandset/doc-buildversion.txt

@ -1,3 +1,4 @@
0.1.0
0.1.1
#First line must be a semantic version number
#all other lines are ignored.
#0.1.1 - fixed treefilenames call using -antiglob_paths instead of -exclude-paths

23
src/modules/punk/mix/commandset/project-999999.0a1.0.tm

@ -386,37 +386,38 @@ namespace eval punk::mix::commandset::project {
src/PROJECT_LAYOUTS_*
}]
#set exclude_dirsegments [list {*}{
#set exclude_paths [list {*}{
# _ignore_*
# **/_ignore_*
#}]
set exclude_dirsegments [list {*}{
set exclude_paths [list {*}{
}]
#default -exclude-dirsegments_core will stop .fossil* from being updated - which is generally desirable as these are likely to be customized
#default -exclude-paths-core will stop .fossil* from being updated - which is generally desirable as these are likely to be customized
if {$opt_force} {
puts stdout "copying layout files - with force applied - overwrite all-targets"
set resultdict [punkcheck::install $layout_path $projectdir -installer project.new -createempty 1 -overwrite ALL-TARGETS -antiglob_paths $antipaths -exclude-dirsegments $exclude_dirsegments]
set resultdict [punkcheck::install $layout_path $projectdir -installer project.new -createempty 1 -overwrite ALL-TARGETS -exclude-paths [concat $antipaths $exclude_paths]]
} else {
puts stdout "copying layout files - (if source file changed)"
set resultdict [punkcheck::install $layout_path $projectdir -installer project.new -createempty 1 -overwrite installedsourcechanged-targets -antiglob_paths $antipaths -exclude-dirsegments $exclude_dirsegments]
set resultdict [punkcheck::install $layout_path $projectdir -installer project.new -createempty 1 -overwrite installedsourcechanged-targets -exclude-paths [concat $antipaths $exclude_paths]]
}
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
if {[file exists $layout_path/src/doc]} {
puts stdout "copying layout src/doc files (if target missing)"
set resultdict [punkcheck::install $layout_path/src/doc $projectdir/src/doc -createdir 1 -punkcheck_folder $projectdir -installer project.new -overwrite SYNCED-TARGETS]
set resultdict [punkcheck::install $layout_path/src/doc $projectdir/src/doc -createdir 1 -punkcheck-folder $projectdir -installer project.new -overwrite SYNCED-TARGETS]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
} else {
puts stdout "no src/doc in source template - update not required"
}
#target folders .fossil-custom and .fossil-settings may not exist. use -createdir 1 to ensure existence.
#In this case we need to override the default dir antiglob - as .fossil-xxx folders need to be installed from template if missing, or if target is uncustomized.
## default_exclude_dirsegments_core [list "#*" "_aside" ".git" ".fossil*"]
set override_exclude_dirsegments_core [list #* _aside .git]
#In this case we need to override the default exclude-paths_core - as .fossil-xxx folders need to be installed from template if missing, or if target is uncustomized.
## default_excludepaths_core [list "#*" "**/#*" "_aside" "**/_aside" "_build" "**/_build" ".git" "**/.git" ".fossil*" "**/.fossil*"]
set override_excludepaths_core [list #* **/#* _aside **/_aside .git **/.git]
if {[file exists $layout_path/.fossil-custom]} {
puts stdout "copying layout src/.fossil-custom files (if target missing or uncustomised)"
set resultdict [punkcheck::install $layout_path/.fossil-custom $projectdir/.fossil-custom -createdir 1 -createempty 1 -punkcheck_folder $projectdir -installer project.new -exclude-dirsegments_core $override_exclude_dirsegments_core -overwrite SYNCED-TARGETS]
set resultdict [punkcheck::install $layout_path/.fossil-custom $projectdir/.fossil-custom -createdir 1 -createempty 1 -punkcheck-folder $projectdir -installer project.new -exclude-paths-core $override_excludepaths_core -overwrite SYNCED-TARGETS]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
} else {
puts stdout "no .fossil-custom in source template - update not required"
@ -424,7 +425,7 @@ namespace eval punk::mix::commandset::project {
if {[file exists $layout_path/.fossil-settings]} {
puts stdout "copying layout src/.fossil-settings files (if target missing or uncustomised)"
set resultdict [punkcheck::install $layout_path/.fossil-settings $projectdir/.fossil-settings -createdir 1 -createempty 1 -punkcheck_folder $projectdir -installer project.new -exclude-dirsegments_core $override_exclude_dirsegments_core -overwrite SYNCED-TARGETS]
set resultdict [punkcheck::install $layout_path/.fossil-settings $projectdir/.fossil-settings -createdir 1 -createempty 1 -punkcheck-folder $projectdir -installer project.new -exclude-paths-core $override_excludepaths_core -overwrite SYNCED-TARGETS]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
} else {
puts stdout "no .fossil-settings in source template - update not required"

4
src/modules/punk/mix/commandset/project-buildversion.txt

@ -1,3 +1,5 @@
0.1.0
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#0.2.0 - updated punkcheck::install call sites to use -exclude-paths / -exclude-paths-core (replaced -antiglob-paths + -exclude-dirsegments) — call-site API change warrants minor bump
#0.2.0 - updated call sites to use hyphenated flag names (-punkcheck-folder, -exclude-paths-core)

91
src/modules/punk/path-999999.0a1.0.tm

@ -1231,6 +1231,14 @@ namespace eval punk::path {
puts stderr "treefilenames error while listing files in dir $opt_dir\n $matches"
set dirfiles [list]
} else {
#also match hidden files (e.g dotfiles on windows where glob -type f misses them)
if {![catch {glob -nocomplain -dir $opt_dir -types {hidden f} -- {*}$tailglobs} hidden_matches]} {
foreach h $hidden_matches {
if {$h ni $matches} {
lappend matches $h
}
}
}
set dirfiles [_retain_files $matches $opt_exclude_files $opt_sort]
}
lappend files {*}[_tailbase_relative_list $opt_tailbase $dirfiles]
@ -1244,6 +1252,19 @@ namespace eval punk::path {
puts stderr "treefilenames error while listing subdirs in dir $opt_dir\n $dirdirs"
set dirdirs [list]
}
#also match hidden directories
if {![catch {glob -nocomplain -dir $opt_dir -types {hidden d} *} hidden_dirdirs]} {
foreach h $hidden_dirdirs {
switch -- $h {
"." - ".." {continue}
default {
if {$h ni $dirdirs} {
lappend dirdirs $h
}
}
}
}
}
set okdirs [list]
foreach dir $dirdirs {
if {![_path_matches_any $opt_exclude_paths [_tailbase_match_path $opt_tailbase $dir]]} {
@ -1444,52 +1465,34 @@ namespace eval punk::path {
return [treefile_priv::walk_treefilenames_zipfs $state]
}
punk::args::define {
@id -id ::punk::path::relative
@cmd -name punk::path::relative\
-summary\
"Compute the relative path from a reference path to a location path."\
-help\
"Taking two directory paths, a reference and a location, computes the path
of the location relative to the reference.
Will return a single dot '.' if the paths are the same.
Both paths must be the same type - ie both absolute or both relative.
Matching is case sensitive. On windows, the drive-letter component (only) is
not case sensitive, so punk::path::relative c:/etc C:/etc returns '.'.
The part following the driveletter is case sensitive, so
punk::path::relative c:/etc C:/Etc returns ../Etc.
On windows, if the paths are absolute and specify different volumes,
only the location will be returned."
@leaders -min 2 -max 2
reference -type string -help\
"The path from which the relative path to location is determined."
location -type string -help\
"The location path which may be above or below the reference path."
}
#maint warning - also in punkcheck
proc relative {reference location} {
#*** !doctools
#[call [fun relative] [arg reference] [arg location]]
#[para] Taking two directory paths, a reference and a location, computes the path
# of the location relative to the reference.
#[list_begin itemized]
#[item]
#[para] Arguments:
# [list_begin arguments]
# [arg_def string reference] The path from which the relative path to location is determined.
# [arg_def string location] The location path which may be above or below the reference path
# [list_end]
#[item]
#[para] Results:
#[para] The relative path of the location to the reference path.
#[para] Will return a single dot "." if the paths are the same
#[item]
#[para] Notes:
#[para] Both paths must be the same type - ie both absolute or both relative
#[para] Case sensitive. ie punk::path::relative /etc /etC
# will return ../etC
#[para] On windows, the drive-letter component (only) is not case sensitive
#[example_begin]
# P% punk::path::relative c:/etc C:/etc
# - .
#[example_end]
#[para] The part following the driveletter is case sensitive so in the following cases it recognises the driveletter matches but not the tail
#[example_begin]
# P% punk::path::relative c:/etc C:/Etc
# - ../Etc
#[example_end]
#[para] On windows, if the paths are absolute and specifiy different volumes, only the location will be returned.
#[example_begin]
# P% punk::path::relative c:/etc d:/etc/blah
# - d:/etc/blah
#[example_end]
#[para] Unix-like examples:
#[example_begin]
# P% punk::path::relative /usr/local/etc/ /usr/local/etc/somewhere/below
# - somewhere/below
# P% punk::path::relative /usr/local/etc/somewhere /usr/local/lib/here
# - ../../lib/here
#[example_end]
#[list_end]
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).

4
src/modules/punk/path-buildversion.txt

@ -1,3 +1,5 @@
0.1.0
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#0.2.0 - treefilenames now matches hidden files and recurses into hidden directories on all platforms
#0.2.0 - added PUNKARGS documentation for punk::path::relative

333
src/modules/punkcheck-999999.0a1.0.tm

@ -41,9 +41,14 @@ namespace eval punkcheck {
summarize_install_resultdict
}
#exclude-dir & exclude-file entries match the pattern at any level - should not contain path separators
variable default_excludedirseg_core [list "#*" "_aside" "_build" ".git" ".fossil*"]
#exclude-paths entries match against the full relative path using globmatchpath.
#They may include ** to match across path segments and / to match within a segment.
#Each dir-seg pattern is expanded to both top-level and nested (**/) forms to preserve
#the "matches at any directory level" semantics of the former dir-seg mechanism.
variable default_excludepaths_core [list "#*" "**/#*" "_aside" "**/_aside" "_build" "**/_build" ".git" "**/.git" ".fossil*" "**/.fossil*"]
variable default_excludefiletail_core ""
#backward-compat: retained for callers that still read default_excludedirseg_core
variable default_excludedirseg_core [list "#*" "_aside" "_build" ".git" ".fossil*"]
set has_twapi 0
if {"windows" eq $::tcl_platform(platform)} {
@ -56,6 +61,11 @@ namespace eval punkcheck {
interp alias "" ::punkcheck::uuid "" ::uuid::uuid generate
}
proc default_excludepaths_core {} {
variable default_excludepaths_core
return $default_excludepaths_core
}
#backward-compat alias for the former segment-based default
proc default_excludedirseg_core {} {
variable default_excludedirseg_core
return $default_excludedirseg_core
@ -1181,84 +1191,9 @@ namespace eval punkcheck {
#maint warning - also in punk::mix::util
# Delegate to punk::path::relative (verified equivalent implementation)
proc path_relative {base dst} {
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).
#
# Taking two _directory_ paths, a base and a destination, computes the path
# of the destination relative to the base.
#
# Arguments:
# base The path to make the destination relative to.
# dst The destination path
#
# Results:
# The path of the destination, relative to the base.
# Ensure that the link to directory 'dst' is properly done relative to
# the directory 'base'.
#review - check volume info on windows.. UNC paths?
if {[file pathtype $base] ne [file pathtype $dst]} {
return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)"
}
#avoid normalizing if possible - at least for relative paths which we are likely to loop on (file normalize *very* expensive on windows)
set do_normalize 0
if {[file pathtype $base] eq "relative"} {
#if base is relative so is dst
if {[regexp {[.]{2}} [list $base $dst]]} {
set do_normalize 1
}
if {[regexp {[.]/} [list $base $dst]]} {
set do_normalize 1
}
} else {
#case differences in volumes is common on windows
set do_normalize 1
}
if {$do_normalize} {
set base [file normalize $base]
set dst [file normalize $dst]
}
set save $dst
set base [file split $base]
set dst [file split $dst]
while {[lindex $dst 0] eq [lindex $base 0]} {
set dst [lrange $dst 1 end]
set base [lrange $base 1 end]
if {![llength $dst]} {break}
}
set dstlen [llength $dst]
set baselen [llength $base]
if {($dstlen == 0) && ($baselen == 0)} {
# Cases:
# (a) base == dst
set dst .
} else {
# Cases:
# (b) base is: base/sub = sub
# dst is: base = {}
# (c) base is: base = {}
# dst is: base/sub = sub
while {$baselen > 0} {
#set dst [linsert $dst 0 ..]
ledit dst -1 -1 ..
incr baselen -1
}
set dst [file join {*}$dst]
}
return $dst
return [punk::path::relative $base $dst]
}
}
#skip writing punkcheck during checksum/timestamp checks
@ -1276,16 +1211,15 @@ namespace eval punkcheck {
}
proc install_non_tm_files {srcdir basedir args} {
#set keys [dict keys $args]
#adjust the default excludedirseg_core entries so that .fossil-custom, .fossil-settings are copied
set excludedirseg_core [punkcheck::default_excludedirseg_core]
set posn [lsearch $excludedirseg_core ".fossil*"]
if {$posn >=0} {
ledit excludedirseg_core $posn $posn
}
#adjust the default exclude-paths_core entries so that .fossil-custom, .fossil-settings are copied
set excludepaths_core [punkcheck::default_excludepaths_core]
#remove .fossil* entries (both top-level and **/ forms) so fossil folders are not excluded
set excludepaths_core [lsearch -inline -all -not $excludepaths_core ".fossil*"]
set excludepaths_core [lsearch -inline -all -not $excludepaths_core "**/.fossil*"]
set defaults [list {*}{
} -glob * {*}{
} -exclude-filetails [list "*.tm" "*-buildversion.txt" "*.exe"] {*}{
} -exclude-dirsegments_core $excludedirseg_core {*}{
} -exclude-paths-core $excludepaths_core {*}{
} -installer punkcheck::install_non_tm_files {*}{
}
]
@ -1324,7 +1258,7 @@ namespace eval punkcheck {
tgtdir -type directory
-call-depth-internal -type integer -default 0 -help "(internal recursion tracker)"
-subdirlist -type list -default "" -help "(primarily internal - length generally matching -call-depth-internal)"
-max_depth -type integer -default 1000 -help\
-max-depth -type integer -default 1000 -help\
"Deepest subdirectory - use -1 for no limit."
-createdir -type boolean -default 0 -help\
"Whether to create the folder at tgtdir.
@ -1333,11 +1267,45 @@ namespace eval punkcheck {
"Whether to create folders at target that had no matches for our glob"
-glob -type string -default "*" -help\
"Pattern matching for source file(s) to copy. Can be glob based or exact match."
-exclude-filetails_core -default {${[::punkcheck::default_excludefiltail_core]}}
-exclude-filetails -default ""
-exclude-dirsegments_core -default {${[::punkcheck::default_excludedirseg_core]}}
-exclude-dirsegments -default ""
-antiglob_paths -default {}
-exclude-filetails-core -default {${[::punkcheck::default_excludefiletail_core]}} -help\
"Non-overridable default file-tail exclusion patterns.
These are applied in addition to -exclude-filetails and cannot be
disabled by the caller - they can only be replaced by passing
-exclude-filetails-core with a different list.
Patterns use string match against the file tail (basename) at any
level in the tree. They must not contain path separators."
-exclude-filetails -default "" -help\
"List of glob patterns to match against file tails (basenames).
Patterns use string match and must not contain path separators.
e.g {*.swp *.tmp} excludes swap and temp files at any level."
-exclude-paths-core -default {${[::punkcheck::default_excludepaths_core]}} -help\
"Non-overridable default path-glob exclusion patterns.
These are applied in addition to -exclude-paths and cannot be
disabled by the caller - they can only be replaced by passing
-exclude-paths-core with a different list.
Patterns are matched against the full relative source path using
punk::path::globmatchpath."
-exclude-paths -default {} -help\
"List of path-glob patterns to exclude from installation.
Patterns are matched against the full relative source path using
punk::path::globmatchpath, so they may include ** to match across
path segments and / to match within a segment.
e.g to exclude all AGENTS.md files at any depth: {AGENTS.md **/AGENTS.md}"
#backward-compat aliases (accepted but deprecated - converted internally to -exclude-paths)
-exclude-dirsegments-core -default {${[::punkcheck::default_excludedirseg_core]}} -help\
"DEPRECATED: use -exclude-paths-core instead.
Legacy segment-glob patterns matched against directory tail names.
Converted internally to path-glob form by expanding each segment
S to {S **/S}."
-exclude-dirsegments -default "" -help\
"DEPRECATED: use -exclude-paths instead.
Legacy segment-glob patterns matched against directory tail names
at any level. Each pattern must not contain path separators."
-antiglob-paths -default {} -help\
"DEPRECATED: use -exclude-paths instead.
Legacy path-glob patterns matched against the full relative source
path using punk::path::globmatchpath. Merged into -exclude-paths
internally."
-overwrite -default no-targets\
-choices {no-targets newer-targets older-targets all-targets installedsourcechanged-targets synced-targets}\
-choicecolumns 1\
@ -1353,15 +1321,22 @@ namespace eval punkcheck {
synced-targets "copy if the target doesn't exist or the source changed
and the target cksum is the same as the last INSTALL-RECORD -targets_cksums entry"
}
-source_checksum -default comparestore -choicecolumns 3 -choices {compare store comparestore false true}\
-choicelabels {
true "same as comparestore"
}
-punkcheck_folder -default target -choices {target source project} -choicerestricted 0 -help\
-source-checksum -default comparestore -choicecolumns 3 -choices {compare store comparestore false true} -help\
"Checksum behavior for source files.
compare: checksum and compare against last record, but do not store.
store: checksum and store, but do not compare against last record.
comparestore: checksum, compare against last record, and store.
false: do not checksum (not recommended - changes cannot be detected).
true: same as comparestore."
-punkcheck-folder -default target -choices {target source project} -choicerestricted 0 -help\
"The location of the .punkcheck file to track installations and checksums.
The default value 'target' is generally recommended.
Can also be an absolute path to a folder."
-punkcheck_records -default "" -help\
-punkcheck-eventid -default "" -help\
"Event ID for the install operation. When empty (default), a new UUID
is generated automatically. Pass a specific ID to group multiple
install calls under the same event in the .punkcheck file."
-punkcheck-records -default "" -help\
"Empty string or a parsed TDL records structure.
e.g
{tag FILEINFO -<opt> <val>... body {
@ -1404,28 +1379,54 @@ namespace eval punkcheck {
# todo - determine what happens if mismatch between file type of a src vs target e.g target has dir matching filename at source
# A pre-scan to determine no such conflict - before attempting to copy anything might provide the most integrity at slight cost in speed.
# REVIEW we should only expect dirs to be created as necessary to hold files? i.e target folder won't be created if no source file matches for that folder
# -source_checksum compare|store|comparestore|false|true where true == comparestore
# -punkcheck_folder target|source|project|<absolutepath> target is default and is generally recommended
# -punkcheck_records empty string | parsed TDL records ie {tag xxx k v} structure
# -source-checksum compare|store|comparestore|false|true where true == comparestore
# -punkcheck-folder target|source|project|<absolutepath> target is default and is generally recommended
# -punkcheck-records empty string | parsed TDL records ie {tag xxx k v} structure
# install creates FILEINFO records with a single entry in the -targets field (it is legitimate to have a list of targets for an installation operation - the oo interface supports this)
proc install {srcdir tgtdir args} {
# Backward-compat: normalize old underscore flag names to hyphenated forms
set flag_aliases [dict create {*}{
-max_depth -max-depth
-exclude-filetails_core -exclude-filetails-core
-exclude-paths_core -exclude-paths-core
-exclude-dirsegments_core -exclude-dirsegments-core
-antiglob_paths -antiglob-paths
-source_checksum -source-checksum
-punkcheck_folder -punkcheck-folder
-punkcheck_eventid -punkcheck-eventid
-punkcheck_records -punkcheck-records
}]
if {[llength $args]} {
set normalized [list]
foreach {k v} $args {
if {[dict exists $flag_aliases $k]} {
lappend normalized [dict get $flag_aliases $k] $v
} else {
lappend normalized $k $v
}
}
set args $normalized
}
set defaults [list {*}{
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-createdir 0
-createempty 0
-glob *
-exclude-filetails_core "\uFFFF"
-exclude-filetails-core "\uFFFF"
-exclude-filetails ""
-exclude-dirsegments_core "\uFFFF"
-exclude-paths-core "\uFFFF"
-exclude-paths {}
-exclude-dirsegments-core "\uFFFF"
-exclude-dirsegments {}
-antiglob_paths {}
-antiglob-paths {}
-overwrite no-targets
-source_checksum comparestore
-punkcheck_folder target
-punkcheck_eventid "\uFFFF"
-punkcheck_records ""
-source-checksum comparestore
-punkcheck-folder target
-punkcheck-eventid "\uFFFF"
-punkcheck-records ""
-installer punkcheck::install
-progresschannel none
}]
@ -1447,7 +1448,7 @@ namespace eval punkcheck {
#and may be less error prone than doing slightly more opaque path manipulations at each recursion level to determine where we started
#For consistency - we'll use the same mechanism in various recursive directory walking procedures such as this one.
set CALLDEPTH [dict get $opts -call-depth-internal] ;#added for extra debug/sanity checking - clearer test for initial function call ie CALLDEPTH = 0
set max_depth [dict get $opts -max_depth] ;# -1 for no limit
set max_depth [dict get $opts -max-depth] ;# -1 for no limit
set subdirlist [dict get $opts -subdirlist] ;# generally should be same length as CALLDEPTH - but user could prefill
set fileglob [dict get $opts -glob]
set createdir [dict get $opts -createdir] ;#defaults to zero to help avoid mistakes with initial target dir - required target subdirs are created regardless of this setting
@ -1474,10 +1475,10 @@ namespace eval punkcheck {
#now the values we build from these will be properly cased
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_excludefiletail_core [dict get $opts -exclude-filetails_core]
set opt_excludefiletail_core [dict get $opts -exclude-filetails-core]
if {$opt_excludefiletail_core eq "\uFFFF"} {
set opt_excludefiletail_core [default_excludefiletail_core]
dict set opts -exclude-filetails_core $opt_excludefiletail_core
dict set opts -exclude-filetails-core $opt_excludefiletail_core
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_excludefiletail [dict get $opts -exclude-filetails]
@ -1488,12 +1489,35 @@ namespace eval punkcheck {
}
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_excludedirseg_core [dict get $opts -exclude-dirsegments_core]
# Unified -exclude-paths initialization (replaces -exclude-dirsegments + -antiglob-paths)
# Precedence: -exclude-paths-core > -exclude-dirsegments-core (legacy) > default
set opt_excludepaths_core [dict get $opts -exclude-paths-core]
set legacy_excludedirseg_core [dict get $opts -exclude-dirsegments-core]
if {$opt_excludepaths_core ne "\uFFFF"} {
# User explicitly passed -exclude-paths-core → use as-is (path-glob form)
} elseif {$legacy_excludedirseg_core ne "\uFFFF"} {
# User explicitly passed legacy -exclude-dirsegments-core → convert to path-glob form
set opt_excludepaths_core [list]
foreach seg $legacy_excludedirseg_core {
lappend opt_excludepaths_core $seg
lappend opt_excludepaths_core **/$seg
}
dict set opts -exclude-paths-core $opt_excludepaths_core
} else {
# Neither passed → use new default
set opt_excludepaths_core [default_excludepaths_core]
dict set opts -exclude-paths-core $opt_excludepaths_core
}
set opt_excludepaths [dict get $opts -exclude-paths]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
# Backward-compat: convert legacy -exclude-dirsegments to path-glob form and merge
# Each segment-glob S is expanded to {S **/S} to match at any directory level,
# preserving the former string-match-against-tail semantics.
set opt_excludedirseg_core $legacy_excludedirseg_core
if {$opt_excludedirseg_core eq "\uFFFF"} {
set opt_excludedirseg_core [default_excludedirseg_core]
dict set opts -exclude-dirsegments_core $opt_excludedirseg_core
dict set opts -exclude-dirsegments-core $opt_excludedirseg_core
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_excludedirseg [dict get $opts -exclude-dirsegments]
#validate no path seps
foreach ad $opt_excludedirseg {
@ -1501,9 +1525,18 @@ namespace eval punkcheck {
error "punkcheck::install received invalid -exclude-dirsegments entry '$ad'. -exclude-dirsegments entries are meant to match to a directory name at any level so cannot contain path separators"
}
}
#convert legacy dir-seg user globs (NOT core) to path-glob form and append to opt_excludepaths
foreach seg $opt_excludedirseg {
lappend opt_excludepaths $seg
lappend opt_excludepaths **/$seg
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_antiglob_paths [dict get $opts -antiglob_paths] ;#todo - combine with config file in source tree .punkcheckpublish (?)
# Backward-compat: merge legacy -antiglob-paths into opt_excludepaths
set opt_antiglob_paths [dict get $opts -antiglob-paths] ;#todo - combine with config file in source tree .punkcheckpublish (?)
#antiglob_paths will usually contain file separators - and may contain glob patterns within each segment
foreach ap $opt_antiglob_paths {
lappend opt_excludepaths $ap
}
set antiglob_paths_matched [list]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set known_whats [list no-targets newer-targets older-targets all-targets installedsourcechanged-targets synced-targets]
@ -1517,17 +1550,17 @@ namespace eval punkcheck {
# external pkg? use twapi and ctime only on other platforms?
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_source_checksum [dict get $opts -source_checksum]
set opt_source_checksum [dict get $opts -source-checksum]
if {[string is boolean $opt_source_checksum]} {
if {$opt_source_checksum} {
set opt_source_checksum "comparestore"
} else {
set opt_source_checksum 0
}
dict set opts -source_checksum $opt_source_checksum
dict set opts -source-checksum $opt_source_checksum
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_punkcheck_folder [dict get $opts -punkcheck_folder]
set opt_punkcheck_folder [dict get $opts -punkcheck-folder]
if {$opt_punkcheck_folder eq "target"} {
set punkcheck_folder $tgtdir
} elseif {$opt_punkcheck_folder eq "source"} {
@ -1540,28 +1573,28 @@ namespace eval punkcheck {
if {$srcproj eq $tgtproj} {
set punkcheck_folder $tgtproj
} else {
error "copy_files_from_source_to_target error: Unable to find common project dir for source and target folder - use absolutepath for -punkcheck_folder if source and target are not within same project"
error "copy_files_from_source_to_target error: Unable to find common project dir for source and target folder - use absolutepath for -punkcheck-folder if source and target are not within same project"
}
} else {
set punkcheck_folder $opt_punkcheck_folder
}
if {$punkcheck_folder ne ""} {
if {[file pathtype $punkcheck_folder] ne "absolute"} {
error "copy_files_from_source_to_target error: -punkcheck_folder '$punkcheck_folder' must be an absolute path, or one of: target|source|project"
error "copy_files_from_source_to_target error: -punkcheck-folder '$punkcheck_folder' must be an absolute path, or one of: target|source|project"
}
if {![file isdirectory $punkcheck_folder]} {
error "copy_files_from_source_to_target error: -punkcheck_folder '$punkcheck_folder' not found"
error "copy_files_from_source_to_target error: -punkcheck-folder '$punkcheck_folder' not found"
}
} else {
#review - leave empty? use pwd?
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set punkcheck_records [dict get $opts -punkcheck_records]
set punkcheck_records [dict get $opts -punkcheck-records]
set punkcheck_records_init $punkcheck_records ;#change-detection
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_installer [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_punkcheck_eventid [dict get $opts -punkcheck_eventid]
set opt_punkcheck_eventid [dict get $opts -punkcheck-eventid]
@ -1572,7 +1605,7 @@ namespace eval punkcheck {
if {$punkcheck_folder ne ""} {
set config $opts
dict unset config -call-depth-internal
dict unset config -max_depth
dict unset config -max-depth
dict unset config -subdirlist
dict unset config -progresschannel
tcl::dict::for {k v} $config {
@ -1699,9 +1732,9 @@ namespace eval punkcheck {
set relative_source_path [file join $relative_source_dir $m]
set punkcheck_target_relpath [file join $target_relative_to_punkcheck_dir $m]
set is_antipath 0
foreach antipath $opt_antiglob_paths {
#puts "testing file - globmatchpath $antipath vs $relative_source_path"
if {[punk::path::globmatchpath $antipath $relative_source_path]} {
foreach pathglob [concat $opt_excludepaths_core $opt_excludepaths] {
#puts "testing file - globmatchpath $pathglob vs $relative_source_path"
if {[punk::path::globmatchpath $pathglob $relative_source_path]} {
lappend antiglob_paths_matched $current_source_dir
set is_antipath 1
break
@ -1968,10 +2001,13 @@ namespace eval punkcheck {
}
#puts stderr "subdirs: $subdirs"
foreach d $subdirs {
set relative_source_path [file join $relative_source_dir $d]
set skipd 0
foreach dg [concat $opt_excludedirseg_core $opt_excludedirseg] {
if {[string match $dg $d]} {
#puts stdout "SKIPPING FOLDER $d due to excludedirseg-match: $dg "
foreach pathglob [concat $opt_excludepaths_core $opt_excludepaths] {
#puts "testing folder - globmatchpath $pathglob vs $relative_source_path"
if {[punk::path::globmatchpath $pathglob $relative_source_path]} {
lappend antiglob_paths_matched [file join $current_source_dir $d]
#puts stdout "SKIPPING FOLDER $relative_source_path due to exclude-path-match: $pathglob "
set skipd 1
break
}
@ -1980,21 +2016,6 @@ namespace eval punkcheck {
continue
}
set relative_source_path [file join $relative_source_dir $d]
set is_antipath 0
foreach antipath $opt_antiglob_paths {
#puts "testing folder - globmatchpath $antipath vs $relative_source_path"
if {[punk::path::globmatchpath $antipath $relative_source_path]} {
lappend antiglob_paths_matched [file join $current_source_dir $d]
#puts stdout "SKIPPING FOLDER $relative_source_path due to antiglob_path-match: $antipath "
set is_antipath 1
break
}
}
if {$is_antipath} {
continue
}
#if {![file exists $current_target_dir/$d]} {
# file mkdir $current_target_dir/$d
#}
@ -2004,24 +2025,24 @@ namespace eval punkcheck {
} -call-depth-internal [expr {$CALLDEPTH + 1}] {*}{
} -subdirlist [list {*}$subdirlist $d] {*}{
} -glob $fileglob {*}{
} -exclude-filetails_core $opt_excludefiletail_core {*}{
} -exclude-filetails-core $opt_excludefiletail_core {*}{
} -exclude-filetails $opt_excludefiletail {*}{
} -exclude-dirsegments_core $opt_excludedirseg_core {*}{
} -exclude-dirsegments $opt_excludedirseg {*}{
} -exclude-paths-core $opt_excludepaths_core {*}{
} -exclude-paths $opt_excludepaths {*}{
} -overwrite $overwrite_what {*}{
} -source_checksum $opt_source_checksum {*}{
} -punkcheck_folder $punkcheck_folder {*}{
} -punkcheck_eventid $punkcheck_eventid {*}{
} -punkcheck_records $punkcheck_records {*}{
} -source-checksum $opt_source_checksum {*}{
} -punkcheck-folder $punkcheck_folder {*}{
} -punkcheck-eventid $punkcheck_eventid {*}{
} -punkcheck-records $punkcheck_records {*}{
} -installer $opt_installer {*}{
}
]
set sub_opts [list {*}{
} -call-depth-internal [expr {$CALLDEPTH + 1}] {*}{
} -subdirlist [list {*}$subdirlist $d] {*}{
} -punkcheck_folder $punkcheck_folder {*}{
} -punkcheck_eventid $punkcheck_eventid {*}{
} -punkcheck_records $punkcheck_records {*}{
} -punkcheck-folder $punkcheck_folder {*}{
} -punkcheck-eventid $punkcheck_eventid {*}{
} -punkcheck-records $punkcheck_records {*}{
} -progresschannel $opt_progresschannel {*}{
}
]

6
src/modules/punkcheck-buildversion.txt

@ -1,3 +1,5 @@
0.1.1
0.2.0
#First line must be a semantic version number
#all other lines are ignored.
#all other lines are ignored.
#0.2.0 - merged -exclude-dirsegments + -antiglob_paths into unified -exclude-paths (backward-compat aliases retained)
#0.2.0 - renamed all underscore flags to hyphenated forms (-max-depth, -source-checksum, -punkcheck-folder, etc.) with backward-compat alias pre-processing

178
src/project_layouts/custom/_project/punk.basic/src/make.tcl

@ -282,6 +282,52 @@ namespace eval ::punkboot::lib {
}
#------------------------------------------------------------------------------
# Bootsupport staleness helpers self-contained Tcl, no punk package deps.
# These run in the early boot phase before punk packages are guaranteed loaded
# (modeled on punk::lib::askuser but without punk::console / raw-mode deps).
#------------------------------------------------------------------------------
# Classify the version delta between an old (bootsupport) and new (source)
# semver as "major", "minor", or "patch".
# Caller must verify new_ver > old_ver via `package vcompare` first.
proc ::punkboot::lib::bootsupport_bump_level {old_ver new_ver} {
set op [split $old_ver .]
set np [split $new_ver .]
while {[llength $op] < 3} {lappend op 0}
while {[llength $np] < 3} {lappend np 0}
set op [lmap s $op {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
set np [lmap s $np {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
lassign $op o_major o_minor o_patch
lassign $np n_major n_minor n_patch
if {$n_major > $o_major} {return "major"}
if {$n_minor > $o_minor} {return "minor"}
return "patch"
}
# Self-contained y/N prompt. Returns 1 for y/yes (case-insensitive), 0 otherwise.
# Used by the staleness abort path to ask the user whether to proceed past a
# minor-version bump. Default is No (Enter = abort) so accidental Enter is safe.
proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
puts stdout $prompt
puts -nonewline stdout "Proceed anyway? (y/N) "
flush stdout
set stdin_state [chan configure stdin]
try {
chan configure stdin -blocking 1
set answer [string tolower [string trim [gets stdin]]]
} finally {
chan configure stdin -blocking [dict get $stdin_state -blocking]
}
return [expr {$answer eq "y" || $answer eq "yes"}]
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -469,6 +515,52 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} {
#package forget punk::repo
#package forget punkcheck
#------------------------------------------------------------------
# Bootsupport staleness check
# When make.tcl is run from project root (not from src/), bootsupport
# modules are loaded in preference to source modules. If the source
# modules have been updated (e.g API changes) but bootsupport hasn't
# been rebuilt, make.tcl will fail with obscure errors from the stale
# bootsupport versions. This check compares buildversion.txt in source
# against the highest .tm version in bootsupport and warns the caller.
#------------------------------------------------------------------
if {[file tail $startdir] ne "src"} {
set _runtime_deps [list {*}{
} punkcheck punkcheck-buildversion.txt punkcheck-*.tm {*}{
} punk::repo punk/repo-buildversion.txt punk/repo-*.tm {*}{
} punk::mix punk/mix-buildversion.txt punk/mix-*.tm {*}{
} punk::tdl punk/tdl-buildversion.txt punk/tdl-*.tm {*}{
}]
set _stale_modules [list]
foreach {pkg buildversion_rel bootsupport_glob} $_runtime_deps {
set _src_bv_file [file join $::punkboot::scriptfolder modules $buildversion_rel]
set _bs_glob_dir [file dirname [file join $::punkboot::scriptfolder bootsupport modules $bootsupport_glob]]
set _bs_glob_pat [file tail $bootsupport_glob]
if {![file exists $_src_bv_file]} {continue}
#read source version (first line of buildversion.txt)
set _fd [open $_src_bv_file r]
set _src_version [string trim [lindex [split [read $_fd] \n] 0]]
close $_fd
#find highest version .tm in bootsupport
set _bs_version ""
set _prefix [string range $_bs_glob_pat 0 [string first "*" $_bs_glob_pat]-1]
foreach _tm [glob -nocomplain -dir $_bs_glob_dir -- $_bs_glob_pat] {
set _v [file rootname [string range [file tail $_tm] [string length $_prefix] end]]
if {$_bs_version eq "" || [package vcompare $_v $_bs_version] == 1} {
set _bs_version $_v
}
}
if {$_bs_version ne "" && $_src_version ne "" && [package vcompare $_src_version $_bs_version] == 1} {
set _bump [::punkboot::lib::bootsupport_bump_level $_bs_version $_src_version]
lappend _stale_modules [list $pkg $_src_version $_bs_version $_bump]
}
}
if {[llength $_stale_modules]} {
set ::punkboot::stale_bootsupport $_stale_modules
}
unset _runtime_deps _stale_modules
}
package require punk::repo ;#todo - push our requirements to a smaller punk::repo::xxx package with minimal dependencies
package require punk::mix
package require punkcheck
@ -1433,6 +1525,51 @@ if {[lsearch $::argv -k] >= 0} {
#puts stdout "::argv $::argv"
# ----------------------------------------
# Abort / prompt if bootsupport modules are stale relative to source.
# Behaviour is version-aware based on the bump level between each stale
# module's bootsupport version and source version:
# - major bump -> abort unconditionally (breaking change, old bootsupport will not work)
# - minor bump -> prompt user; backward-compat aliases *should* keep old bootsupport functional
# - patch bump -> warn and proceed (non-breaking)
# 'check' is exempt in all cases the warning is shown at the end of its output instead.
if {[info exists ::punkboot::stale_bootsupport] && $::punkboot::command ne "check"} {
set _stale $::punkboot::stale_bootsupport
set _have_major 0
set _have_minor 0
set _have_patch 0
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
switch -- $_bump {
major {incr _have_major}
minor {incr _have_minor}
patch {incr _have_patch}
default {incr _have_patch}
}
}
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Recommended fix update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
if {$_have_major} {
puts stderr "-aborted- (major version bump in stale bootsupport breaking change, cannot proceed)"
exit 1
}
if {$_have_minor} {
if {![::punkboot::lib::bootsupport_prompt_yesno "Minor-version staleness backward-compat aliases should keep old bootsupport functional."]} {
puts stderr "-aborted- by user"
exit 1
}
puts stderr "-proceeding with stale bootsupport at user's request-"
}
# patch-only staleness: warn and proceed (warning already printed above)
}
set scriptfolder $::punkboot::scriptfolder
@ -1582,6 +1719,19 @@ if {$::punkboot::command eq "check"} {
puts stdout $sep
catch {package require struct::set}
puts stdout ---[package ifneeded struct::set 2.2.3]
# Show bootsupport staleness warning at the end of check output
if {[info exists ::punkboot::stale_bootsupport]} {
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $::punkboot::stale_bootsupport {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Run the following to update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
}
exit 0
}
@ -2025,7 +2175,7 @@ if {$::punkboot::command eq "bootsupport"} {
set resultdict [punkcheck::install $sourcemodules $targetroot\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-installer "punkboot-bootsupport"\
-progresschannel stdout\
]
@ -2042,7 +2192,7 @@ if {$::punkboot::command eq "bootsupport"} {
puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $source_bootscript to $targetroot_bootscript (if source file changed)"
set resultdict [punkcheck::install [file dirname $source_bootscript] $targetroot_bootscript\
-glob make.tcl\
-max_depth 1\
-max-depth 1\
-createempty 0\
-overwrite installedsourcechanged-targets\
-installer "punkboot-bootsupport"\
@ -2098,11 +2248,11 @@ if {$::punkboot::command in {project packages modules}} {
-overwrite installedsourcechanged-targets
-progresschannel stdout
-exclude-filetails {AGENTS.md include_modules.config}
-antiglob_paths {README.md}
-exclude-paths {README.md}
}]
# -antiglob_paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
# -exclude-paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
#-exclude-filetails {AGENTS.md include_modules.config}
#-antiglob_paths {README.md}
#-exclude-paths {README.md}
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}
if {![llength $vendormodulefolders]} {
@ -2112,7 +2262,7 @@ if {$::punkboot::command in {project packages modules}} {
if {$::punkboot::command in {project packages libs}} {
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2130,7 +2280,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB: copying tcl-version neutral and platform neutral libraries from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2150,7 +2300,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2165,7 +2315,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2183,7 +2333,7 @@ if {$::punkboot::command in {project packages libs}} {
########################################################
lappend projectlibfolders lib
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2200,7 +2350,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB: copying from $sourcefolder/$lf to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/$lf $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2219,7 +2369,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2235,7 +2385,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2358,7 +2508,7 @@ if {$::punkboot::command in {project packages modules}} {
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base\
-installer make.tcl\
-overwrite $overwrite\
-antiglob_paths {README.md **/AGENTS.md AGENTS.md}\
-exclude-paths {README.md **/AGENTS.md AGENTS.md}\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]

7
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm

@ -1325,6 +1325,13 @@ tcl::namespace::eval punk::ansi {
grepstr -r m -C 2 {cancelid} [s ::after] ;#with 2 lines of context and no break indicators.
}]}
To match a literal string that contains regex special characters, it is in some cases easier to use the Tcl regex
quoting switch (?q) to quote the string rather than escaping each special character with a backslash.
${[punk::args::helpers::example -syntax none {
#The following example matches a literal string that is the Tcl expansion operator {*}
grepstr "(?q){*}" $sometext
}]}
}
@leaders -min 0 -max 0

14
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -474,7 +474,7 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-punkcheck_eventobj "\uFFFF"
-glob *.tm
@ -487,7 +487,7 @@ namespace eval punk::mix::cli {
set installername [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set CALLDEPTH [dict get $opts -call-depth-internal]
set max_depth [dict get $opts -max_depth]
set max_depth [dict get $opts -max-depth]
set subdirlist [dict get $opts -subdirlist]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set fileglob [dict get $opts -glob]
@ -533,7 +533,7 @@ namespace eval punk::mix::cli {
set config [dict create {*}{
-glob $fileglob
-max_depth 0
-max-depth 0
}]
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
@ -628,11 +628,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
@ -829,11 +829,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder

1427
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.4.0.tm

File diff suppressed because it is too large Load Diff

324
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm

@ -0,0 +1,324 @@
# -*- 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 punk::mix::commandset::doc 0.1.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require punk::path ;# for treefilenames, relative
package require punk::repo
package require punk::docgen ;#inline doctools - generate doctools .man files at src/docgen prior to using kettle to producing .html .md etc
package require punk::mix::cli ;#punk::mix::cli::lib used for kettle_call
#package require punkcheck ;#for path_relative
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::mix::commandset::doc {
namespace export *
proc _default {} {
puts "documentation subsystem"
puts "commands: doc.build"
puts " build documentation from src/doc to src/embedded using the kettle build tool"
puts "commands: doc.status"
}
proc build {} {
puts "build docs"
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to build docs"
return
}
#user may delete the comment containing "--- punk::docgen::overwrites" and then manually edit, and we won't overwrite
#we still generate output in src/docgen so user can diff and manually update if thats what they prefer
set oldfiles [punk::path::treefilenames -dir $projectdir/src/doc _module_*.man]
foreach maybedoomed $oldfiles {
set fd [open $maybedoomed r]
chan conf $fd -translation binary
set data [read $fd]
close $fd
if {[string match "*--- punk::docgen overwrites *" $data]} {
file delete -force $maybedoomed
}
}
set generated [lib::do_docgen modules]
if {[dict get $generated count] > 0} {
#review
set doclist [dict get $generated docs]
set source_base [dict get $generated base]
set target_base $projectdir/src/doc
foreach dinfo $doclist {
lassign $dinfo module fpath
set relpath [punk::path::relative $source_base $fpath]
set relfolder [file dirname $relpath]
if {$relfolder eq "."} {
set relfolder ""
}
file mkdir [file join $target_base $relfolder]
set target [file join $target_base $relfolder _module_[file tail $fpath]]
puts stderr "target --> $target"
if {![file exists $target]} {
file copy $fpath $target
}
}
}
if {[file exists $projectdir/src/doc]} {
set original_wd [pwd]
cd $projectdir/src
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init VIRTUAL kettle_build_doc ;#VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
if {\
[llength [dict get [$event targetset_source_changes] changed]]\
} {
$event targetset_started
# -- --- --- --- --- ---
puts stdout "BUILDING DOCS at $projectdir/src/embedded from src/doc"
if {[catch {
if {"::meta" eq [info commands ::meta]} {
puts stderr "There appears to be a leftover ::meta command which is presumed to be from doctools. Destroying object"
::meta destroy
}
punk::mix::cli::lib::kettle_call lib doc
#Kettle doc
} errM]} {
$event targetset_end FAILED -note "kettle_build_doc failed: $errM"
} else {
$event targetset_end OK
}
# -- --- --- --- --- ---
} else {
puts stderr "No change detected in src/doc"
$event targetset_end SKIPPED
}
$event end
$event destroy
$installer destroy
cd $original_wd
} else {
puts stderr "No doc folder found at $projectdir/src/doc"
}
}
proc status {} {
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
cd $projectdir/src
puts stdout "Testing status of doctools source location $projectdir/src/doc ..."
flush stdout
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init QUERY kettle_build_doc ;#usually VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated - but here we use QUERY to ensure no writes to .punkcheck
set last_completion [$event targetset_last_complete]
if {[llength $last_completion]} {
#adding a source causes it to be checksummed
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
set changeinfo [$event targetset_source_changes]
if {\
[llength [dict get $changeinfo changed]]\
} {
puts stdout "changed"
puts stdout $changeinfo
} else {
puts stdout "No changes detected in $projectdir/src/doc tree"
}
} else {
#no previous completion-record for this target - must assume changed - no need to trigger checksumming
puts stdout "No existing record of doc build in .punkcheck. Assume it needs to be rebuilt."
}
$event destroy
$installer destroy
cd $original_wd
}
proc validate {args} {
set argd [punk::args::parse $args withdef {
@id -id ::punk::mix::commandset::doc::validate
-- -type none -optional 1 -help "end of options marker --"
-individual -type boolean -default 1
@values -min 0 -max -1
patterns -default {*.man} -type any -multiple 1
}]
set opt_individual [tcl::dict::get $argd opts -individual]
set patterns [tcl::dict::get $argd values patterns]
#todo - run and validate punk::docgen output
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
set docroot $projectdir/src/doc
cd $docroot
if {!$opt_individual && "*.man" in $patterns} {
if {[catch {
dtplite validate $docroot
} errM]} {
puts stderr "commandset::doc::validate failed for projectdir '$projectdir'"
puts stderr "docroot '$docroot'"
puts stderr "dtplite error was: $errM"
}
} else {
foreach p $patterns {
set treefiles [punk::path::treefilenames $p]
foreach path $treefiles {
puts stdout "dtplite validate $path"
dtplite validate $path
}
}
}
#punk::mix::cli::lib::kettle_call lib validate-doc
cd $original_wd
}
namespace eval collection {
variable pkg
set pkg punk::mix::commandset::doc
namespace export *
namespace path [namespace parent]
}
namespace eval lib {
variable pkg
set pkg punk::mix::commandset::doc
proc do_docgen {{project_subpath modules}} {
#Extract doctools comments from source code
set projectdir [punk::repo::find_project]
set output_base [file join $projectdir src docgen]
set codesource_path [file join $projectdir $project_subpath]
if {![file isdirectory $codesource_path]} {
puts stderr "WARNING punk::mix::commandset::doc unable to find codesource_path $codesource_path during do_docgen - skipping inline doctools generation"
return
}
if {[file isdirectory $output_base]} {
if {[catch {
file delete -force $output_base
}]} {
error "do_docgen failed to delete existing output base folder: $output_base"
}
}
file mkdir $output_base
set matched_paths [punk::path::treefilenames -dir $codesource_path -exclude-paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set count 0
set newdocs [list]
set docgen_header_comments ""
append docgen_header_comments {[comment {--- punk::docgen generated from inline doctools comments ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen DO NOT EDIT DOCS HERE UNLESS YOU REMOVE THESE COMMENT LINES ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen overwrites this file ---}]} \n
foreach fullpath $matched_paths {
puts stdout "do_docgen processing: $fullpath"
set doctools [punk::docgen::get_doctools_comments $fullpath]
if {$doctools ne ""} {
set fname [file tail $fullpath]
set mod_tail [file rootname $fname]
set relpath [punk::path::relative $codesource_path [file dirname $fullpath]]
if {$relpath eq "."} {
set relpath ""
}
set tailsegs [file split $relpath]
set module_fullname [join $tailsegs ::]::$mod_tail
set target_docname $fname.man
set this_outdir [file join $output_base $relpath]
if {[string length $fname] > 99} {
#output needs to be tarballed to do checksum change tests in a reasonably straightforward and not-too-terribly slow way.
#hack - review. Determine exact limit - test if tcllib tar fixed or if it's a limit of the particular tar format
#work around tcllib tar filename length limit ( somewhere around 100?) This seems to be a limit on the length of a particular segment in the path.. not whole path length?
#this case only came up because docgen used to path munge to long filenames - but left because we know there is a limit and renaming fixes it - even if it's ugly - but still allows doc generation.
#review - if we're checking fname - should also test length of whole path and determine limits for tar
package require md5
if {[package vsatisfies [package present md5] 2- ] } {
set md5opt "-hex"
} else {
set md5opt ""
}
set target_docname [md5::md5 {*}$md5opt [encoding convertto utf-8 $fullpath]]_overlongfilename.man
puts stderr "WARNING - overlong file name - renaming $fullpath"
puts stderr " to [file dirname $fullpath]/$target_docname"
}
file mkdir $this_outdir
puts stdout "saving [string length $doctools] bytes of doctools output from file $relpath/$fname"
set outfile [file join $this_outdir $target_docname]
set fd [open $outfile w]
fconfigure $fd -translation binary
puts -nonewline $fd $docgen_header_comments$doctools
close $fd
incr count
lappend newdocs [list $module_fullname $outfile]
}
}
return [list count $count docs $newdocs base $output_base]
}
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::mix::commandset::doc [namespace eval punk::mix::commandset::doc {
variable pkg punk::mix::commandset::doc
variable version
set version 0.1.1
}]
return

1178
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm

File diff suppressed because it is too large Load Diff

1178
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm

File diff suppressed because it is too large Load Diff

70
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm

@ -1444,52 +1444,34 @@ namespace eval punk::path {
return [treefile_priv::walk_treefilenames_zipfs $state]
}
punk::args::define {
@id -id ::punk::path::relative
@cmd -name punk::path::relative\
-summary\
"Compute the relative path from a reference path to a location path."\
-help\
"Taking two directory paths, a reference and a location, computes the path
of the location relative to the reference.
Will return a single dot '.' if the paths are the same.
Both paths must be the same type - ie both absolute or both relative.
Matching is case sensitive. On windows, the drive-letter component (only) is
not case sensitive, so punk::path::relative c:/etc C:/etc returns '.'.
The part following the driveletter is case sensitive, so
punk::path::relative c:/etc C:/Etc returns ../Etc.
On windows, if the paths are absolute and specify different volumes,
only the location will be returned."
@leaders -min 2 -max 2
reference -type string -help\
"The path from which the relative path to location is determined."
location -type string -help\
"The location path which may be above or below the reference path."
}
#maint warning - also in punkcheck
proc relative {reference location} {
#*** !doctools
#[call [fun relative] [arg reference] [arg location]]
#[para] Taking two directory paths, a reference and a location, computes the path
# of the location relative to the reference.
#[list_begin itemized]
#[item]
#[para] Arguments:
# [list_begin arguments]
# [arg_def string reference] The path from which the relative path to location is determined.
# [arg_def string location] The location path which may be above or below the reference path
# [list_end]
#[item]
#[para] Results:
#[para] The relative path of the location to the reference path.
#[para] Will return a single dot "." if the paths are the same
#[item]
#[para] Notes:
#[para] Both paths must be the same type - ie both absolute or both relative
#[para] Case sensitive. ie punk::path::relative /etc /etC
# will return ../etC
#[para] On windows, the drive-letter component (only) is not case sensitive
#[example_begin]
# P% punk::path::relative c:/etc C:/etc
# - .
#[example_end]
#[para] The part following the driveletter is case sensitive so in the following cases it recognises the driveletter matches but not the tail
#[example_begin]
# P% punk::path::relative c:/etc C:/Etc
# - ../Etc
#[example_end]
#[para] On windows, if the paths are absolute and specifiy different volumes, only the location will be returned.
#[example_begin]
# P% punk::path::relative c:/etc d:/etc/blah
# - d:/etc/blah
#[example_end]
#[para] Unix-like examples:
#[example_begin]
# P% punk::path::relative /usr/local/etc/ /usr/local/etc/somewhere/below
# - somewhere/below
# P% punk::path::relative /usr/local/etc/somewhere /usr/local/lib/here
# - ../../lib/here
#[example_end]
#[list_end]
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).

1611
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.2.0.tm

File diff suppressed because it is too large Load Diff

2479
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.2.0.tm

File diff suppressed because it is too large Load Diff

178
src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl

@ -282,6 +282,52 @@ namespace eval ::punkboot::lib {
}
#------------------------------------------------------------------------------
# Bootsupport staleness helpers self-contained Tcl, no punk package deps.
# These run in the early boot phase before punk packages are guaranteed loaded
# (modeled on punk::lib::askuser but without punk::console / raw-mode deps).
#------------------------------------------------------------------------------
# Classify the version delta between an old (bootsupport) and new (source)
# semver as "major", "minor", or "patch".
# Caller must verify new_ver > old_ver via `package vcompare` first.
proc ::punkboot::lib::bootsupport_bump_level {old_ver new_ver} {
set op [split $old_ver .]
set np [split $new_ver .]
while {[llength $op] < 3} {lappend op 0}
while {[llength $np] < 3} {lappend np 0}
set op [lmap s $op {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
set np [lmap s $np {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
lassign $op o_major o_minor o_patch
lassign $np n_major n_minor n_patch
if {$n_major > $o_major} {return "major"}
if {$n_minor > $o_minor} {return "minor"}
return "patch"
}
# Self-contained y/N prompt. Returns 1 for y/yes (case-insensitive), 0 otherwise.
# Used by the staleness abort path to ask the user whether to proceed past a
# minor-version bump. Default is No (Enter = abort) so accidental Enter is safe.
proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
puts stdout $prompt
puts -nonewline stdout "Proceed anyway? (y/N) "
flush stdout
set stdin_state [chan configure stdin]
try {
chan configure stdin -blocking 1
set answer [string tolower [string trim [gets stdin]]]
} finally {
chan configure stdin -blocking [dict get $stdin_state -blocking]
}
return [expr {$answer eq "y" || $answer eq "yes"}]
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -469,6 +515,52 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} {
#package forget punk::repo
#package forget punkcheck
#------------------------------------------------------------------
# Bootsupport staleness check
# When make.tcl is run from project root (not from src/), bootsupport
# modules are loaded in preference to source modules. If the source
# modules have been updated (e.g API changes) but bootsupport hasn't
# been rebuilt, make.tcl will fail with obscure errors from the stale
# bootsupport versions. This check compares buildversion.txt in source
# against the highest .tm version in bootsupport and warns the caller.
#------------------------------------------------------------------
if {[file tail $startdir] ne "src"} {
set _runtime_deps [list {*}{
} punkcheck punkcheck-buildversion.txt punkcheck-*.tm {*}{
} punk::repo punk/repo-buildversion.txt punk/repo-*.tm {*}{
} punk::mix punk/mix-buildversion.txt punk/mix-*.tm {*}{
} punk::tdl punk/tdl-buildversion.txt punk/tdl-*.tm {*}{
}]
set _stale_modules [list]
foreach {pkg buildversion_rel bootsupport_glob} $_runtime_deps {
set _src_bv_file [file join $::punkboot::scriptfolder modules $buildversion_rel]
set _bs_glob_dir [file dirname [file join $::punkboot::scriptfolder bootsupport modules $bootsupport_glob]]
set _bs_glob_pat [file tail $bootsupport_glob]
if {![file exists $_src_bv_file]} {continue}
#read source version (first line of buildversion.txt)
set _fd [open $_src_bv_file r]
set _src_version [string trim [lindex [split [read $_fd] \n] 0]]
close $_fd
#find highest version .tm in bootsupport
set _bs_version ""
set _prefix [string range $_bs_glob_pat 0 [string first "*" $_bs_glob_pat]-1]
foreach _tm [glob -nocomplain -dir $_bs_glob_dir -- $_bs_glob_pat] {
set _v [file rootname [string range [file tail $_tm] [string length $_prefix] end]]
if {$_bs_version eq "" || [package vcompare $_v $_bs_version] == 1} {
set _bs_version $_v
}
}
if {$_bs_version ne "" && $_src_version ne "" && [package vcompare $_src_version $_bs_version] == 1} {
set _bump [::punkboot::lib::bootsupport_bump_level $_bs_version $_src_version]
lappend _stale_modules [list $pkg $_src_version $_bs_version $_bump]
}
}
if {[llength $_stale_modules]} {
set ::punkboot::stale_bootsupport $_stale_modules
}
unset _runtime_deps _stale_modules
}
package require punk::repo ;#todo - push our requirements to a smaller punk::repo::xxx package with minimal dependencies
package require punk::mix
package require punkcheck
@ -1433,6 +1525,51 @@ if {[lsearch $::argv -k] >= 0} {
#puts stdout "::argv $::argv"
# ----------------------------------------
# Abort / prompt if bootsupport modules are stale relative to source.
# Behaviour is version-aware based on the bump level between each stale
# module's bootsupport version and source version:
# - major bump -> abort unconditionally (breaking change, old bootsupport will not work)
# - minor bump -> prompt user; backward-compat aliases *should* keep old bootsupport functional
# - patch bump -> warn and proceed (non-breaking)
# 'check' is exempt in all cases the warning is shown at the end of its output instead.
if {[info exists ::punkboot::stale_bootsupport] && $::punkboot::command ne "check"} {
set _stale $::punkboot::stale_bootsupport
set _have_major 0
set _have_minor 0
set _have_patch 0
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
switch -- $_bump {
major {incr _have_major}
minor {incr _have_minor}
patch {incr _have_patch}
default {incr _have_patch}
}
}
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Recommended fix update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
if {$_have_major} {
puts stderr "-aborted- (major version bump in stale bootsupport breaking change, cannot proceed)"
exit 1
}
if {$_have_minor} {
if {![::punkboot::lib::bootsupport_prompt_yesno "Minor-version staleness backward-compat aliases should keep old bootsupport functional."]} {
puts stderr "-aborted- by user"
exit 1
}
puts stderr "-proceeding with stale bootsupport at user's request-"
}
# patch-only staleness: warn and proceed (warning already printed above)
}
set scriptfolder $::punkboot::scriptfolder
@ -1582,6 +1719,19 @@ if {$::punkboot::command eq "check"} {
puts stdout $sep
catch {package require struct::set}
puts stdout ---[package ifneeded struct::set 2.2.3]
# Show bootsupport staleness warning at the end of check output
if {[info exists ::punkboot::stale_bootsupport]} {
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $::punkboot::stale_bootsupport {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Run the following to update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
}
exit 0
}
@ -2025,7 +2175,7 @@ if {$::punkboot::command eq "bootsupport"} {
set resultdict [punkcheck::install $sourcemodules $targetroot\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-installer "punkboot-bootsupport"\
-progresschannel stdout\
]
@ -2042,7 +2192,7 @@ if {$::punkboot::command eq "bootsupport"} {
puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $source_bootscript to $targetroot_bootscript (if source file changed)"
set resultdict [punkcheck::install [file dirname $source_bootscript] $targetroot_bootscript\
-glob make.tcl\
-max_depth 1\
-max-depth 1\
-createempty 0\
-overwrite installedsourcechanged-targets\
-installer "punkboot-bootsupport"\
@ -2098,11 +2248,11 @@ if {$::punkboot::command in {project packages modules}} {
-overwrite installedsourcechanged-targets
-progresschannel stdout
-exclude-filetails {AGENTS.md include_modules.config}
-antiglob_paths {README.md}
-exclude-paths {README.md}
}]
# -antiglob_paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
# -exclude-paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
#-exclude-filetails {AGENTS.md include_modules.config}
#-antiglob_paths {README.md}
#-exclude-paths {README.md}
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}
if {![llength $vendormodulefolders]} {
@ -2112,7 +2262,7 @@ if {$::punkboot::command in {project packages modules}} {
if {$::punkboot::command in {project packages libs}} {
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2130,7 +2280,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB: copying tcl-version neutral and platform neutral libraries from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2150,7 +2300,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2165,7 +2315,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2183,7 +2333,7 @@ if {$::punkboot::command in {project packages libs}} {
########################################################
lappend projectlibfolders lib
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2200,7 +2350,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB: copying from $sourcefolder/$lf to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/$lf $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2219,7 +2369,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2235,7 +2385,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2358,7 +2508,7 @@ if {$::punkboot::command in {project packages modules}} {
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base\
-installer make.tcl\
-overwrite $overwrite\
-antiglob_paths {README.md **/AGENTS.md AGENTS.md}\
-exclude-paths {README.md **/AGENTS.md AGENTS.md}\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]

7
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm

@ -1325,6 +1325,13 @@ tcl::namespace::eval punk::ansi {
grepstr -r m -C 2 {cancelid} [s ::after] ;#with 2 lines of context and no break indicators.
}]}
To match a literal string that contains regex special characters, it is in some cases easier to use the Tcl regex
quoting switch (?q) to quote the string rather than escaping each special character with a backslash.
${[punk::args::helpers::example -syntax none {
#The following example matches a literal string that is the Tcl expansion operator {*}
grepstr "(?q){*}" $sometext
}]}
}
@leaders -min 0 -max 0

14
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -474,7 +474,7 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-punkcheck_eventobj "\uFFFF"
-glob *.tm
@ -487,7 +487,7 @@ namespace eval punk::mix::cli {
set installername [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set CALLDEPTH [dict get $opts -call-depth-internal]
set max_depth [dict get $opts -max_depth]
set max_depth [dict get $opts -max-depth]
set subdirlist [dict get $opts -subdirlist]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set fileglob [dict get $opts -glob]
@ -533,7 +533,7 @@ namespace eval punk::mix::cli {
set config [dict create {*}{
-glob $fileglob
-max_depth 0
-max-depth 0
}]
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
@ -628,11 +628,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
@ -829,11 +829,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder

1427
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.4.0.tm

File diff suppressed because it is too large Load Diff

324
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/doc-0.1.1.tm

@ -0,0 +1,324 @@
# -*- 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 punk::mix::commandset::doc 0.1.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require punk::path ;# for treefilenames, relative
package require punk::repo
package require punk::docgen ;#inline doctools - generate doctools .man files at src/docgen prior to using kettle to producing .html .md etc
package require punk::mix::cli ;#punk::mix::cli::lib used for kettle_call
#package require punkcheck ;#for path_relative
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::mix::commandset::doc {
namespace export *
proc _default {} {
puts "documentation subsystem"
puts "commands: doc.build"
puts " build documentation from src/doc to src/embedded using the kettle build tool"
puts "commands: doc.status"
}
proc build {} {
puts "build docs"
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to build docs"
return
}
#user may delete the comment containing "--- punk::docgen::overwrites" and then manually edit, and we won't overwrite
#we still generate output in src/docgen so user can diff and manually update if thats what they prefer
set oldfiles [punk::path::treefilenames -dir $projectdir/src/doc _module_*.man]
foreach maybedoomed $oldfiles {
set fd [open $maybedoomed r]
chan conf $fd -translation binary
set data [read $fd]
close $fd
if {[string match "*--- punk::docgen overwrites *" $data]} {
file delete -force $maybedoomed
}
}
set generated [lib::do_docgen modules]
if {[dict get $generated count] > 0} {
#review
set doclist [dict get $generated docs]
set source_base [dict get $generated base]
set target_base $projectdir/src/doc
foreach dinfo $doclist {
lassign $dinfo module fpath
set relpath [punk::path::relative $source_base $fpath]
set relfolder [file dirname $relpath]
if {$relfolder eq "."} {
set relfolder ""
}
file mkdir [file join $target_base $relfolder]
set target [file join $target_base $relfolder _module_[file tail $fpath]]
puts stderr "target --> $target"
if {![file exists $target]} {
file copy $fpath $target
}
}
}
if {[file exists $projectdir/src/doc]} {
set original_wd [pwd]
cd $projectdir/src
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init VIRTUAL kettle_build_doc ;#VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
if {\
[llength [dict get [$event targetset_source_changes] changed]]\
} {
$event targetset_started
# -- --- --- --- --- ---
puts stdout "BUILDING DOCS at $projectdir/src/embedded from src/doc"
if {[catch {
if {"::meta" eq [info commands ::meta]} {
puts stderr "There appears to be a leftover ::meta command which is presumed to be from doctools. Destroying object"
::meta destroy
}
punk::mix::cli::lib::kettle_call lib doc
#Kettle doc
} errM]} {
$event targetset_end FAILED -note "kettle_build_doc failed: $errM"
} else {
$event targetset_end OK
}
# -- --- --- --- --- ---
} else {
puts stderr "No change detected in src/doc"
$event targetset_end SKIPPED
}
$event end
$event destroy
$installer destroy
cd $original_wd
} else {
puts stderr "No doc folder found at $projectdir/src/doc"
}
}
proc status {} {
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
cd $projectdir/src
puts stdout "Testing status of doctools source location $projectdir/src/doc ..."
flush stdout
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init QUERY kettle_build_doc ;#usually VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated - but here we use QUERY to ensure no writes to .punkcheck
set last_completion [$event targetset_last_complete]
if {[llength $last_completion]} {
#adding a source causes it to be checksummed
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
set changeinfo [$event targetset_source_changes]
if {\
[llength [dict get $changeinfo changed]]\
} {
puts stdout "changed"
puts stdout $changeinfo
} else {
puts stdout "No changes detected in $projectdir/src/doc tree"
}
} else {
#no previous completion-record for this target - must assume changed - no need to trigger checksumming
puts stdout "No existing record of doc build in .punkcheck. Assume it needs to be rebuilt."
}
$event destroy
$installer destroy
cd $original_wd
}
proc validate {args} {
set argd [punk::args::parse $args withdef {
@id -id ::punk::mix::commandset::doc::validate
-- -type none -optional 1 -help "end of options marker --"
-individual -type boolean -default 1
@values -min 0 -max -1
patterns -default {*.man} -type any -multiple 1
}]
set opt_individual [tcl::dict::get $argd opts -individual]
set patterns [tcl::dict::get $argd values patterns]
#todo - run and validate punk::docgen output
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
set docroot $projectdir/src/doc
cd $docroot
if {!$opt_individual && "*.man" in $patterns} {
if {[catch {
dtplite validate $docroot
} errM]} {
puts stderr "commandset::doc::validate failed for projectdir '$projectdir'"
puts stderr "docroot '$docroot'"
puts stderr "dtplite error was: $errM"
}
} else {
foreach p $patterns {
set treefiles [punk::path::treefilenames $p]
foreach path $treefiles {
puts stdout "dtplite validate $path"
dtplite validate $path
}
}
}
#punk::mix::cli::lib::kettle_call lib validate-doc
cd $original_wd
}
namespace eval collection {
variable pkg
set pkg punk::mix::commandset::doc
namespace export *
namespace path [namespace parent]
}
namespace eval lib {
variable pkg
set pkg punk::mix::commandset::doc
proc do_docgen {{project_subpath modules}} {
#Extract doctools comments from source code
set projectdir [punk::repo::find_project]
set output_base [file join $projectdir src docgen]
set codesource_path [file join $projectdir $project_subpath]
if {![file isdirectory $codesource_path]} {
puts stderr "WARNING punk::mix::commandset::doc unable to find codesource_path $codesource_path during do_docgen - skipping inline doctools generation"
return
}
if {[file isdirectory $output_base]} {
if {[catch {
file delete -force $output_base
}]} {
error "do_docgen failed to delete existing output base folder: $output_base"
}
}
file mkdir $output_base
set matched_paths [punk::path::treefilenames -dir $codesource_path -exclude-paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set count 0
set newdocs [list]
set docgen_header_comments ""
append docgen_header_comments {[comment {--- punk::docgen generated from inline doctools comments ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen DO NOT EDIT DOCS HERE UNLESS YOU REMOVE THESE COMMENT LINES ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen overwrites this file ---}]} \n
foreach fullpath $matched_paths {
puts stdout "do_docgen processing: $fullpath"
set doctools [punk::docgen::get_doctools_comments $fullpath]
if {$doctools ne ""} {
set fname [file tail $fullpath]
set mod_tail [file rootname $fname]
set relpath [punk::path::relative $codesource_path [file dirname $fullpath]]
if {$relpath eq "."} {
set relpath ""
}
set tailsegs [file split $relpath]
set module_fullname [join $tailsegs ::]::$mod_tail
set target_docname $fname.man
set this_outdir [file join $output_base $relpath]
if {[string length $fname] > 99} {
#output needs to be tarballed to do checksum change tests in a reasonably straightforward and not-too-terribly slow way.
#hack - review. Determine exact limit - test if tcllib tar fixed or if it's a limit of the particular tar format
#work around tcllib tar filename length limit ( somewhere around 100?) This seems to be a limit on the length of a particular segment in the path.. not whole path length?
#this case only came up because docgen used to path munge to long filenames - but left because we know there is a limit and renaming fixes it - even if it's ugly - but still allows doc generation.
#review - if we're checking fname - should also test length of whole path and determine limits for tar
package require md5
if {[package vsatisfies [package present md5] 2- ] } {
set md5opt "-hex"
} else {
set md5opt ""
}
set target_docname [md5::md5 {*}$md5opt [encoding convertto utf-8 $fullpath]]_overlongfilename.man
puts stderr "WARNING - overlong file name - renaming $fullpath"
puts stderr " to [file dirname $fullpath]/$target_docname"
}
file mkdir $this_outdir
puts stdout "saving [string length $doctools] bytes of doctools output from file $relpath/$fname"
set outfile [file join $this_outdir $target_docname]
set fd [open $outfile w]
fconfigure $fd -translation binary
puts -nonewline $fd $docgen_header_comments$doctools
close $fd
incr count
lappend newdocs [list $module_fullname $outfile]
}
}
return [list count $count docs $newdocs base $output_base]
}
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::mix::commandset::doc [namespace eval punk::mix::commandset::doc {
variable pkg punk::mix::commandset::doc
variable version
set version 0.1.1
}]
return

1178
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.1.tm

File diff suppressed because it is too large Load Diff

1178
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.2.0.tm

File diff suppressed because it is too large Load Diff

70
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm

@ -1444,52 +1444,34 @@ namespace eval punk::path {
return [treefile_priv::walk_treefilenames_zipfs $state]
}
punk::args::define {
@id -id ::punk::path::relative
@cmd -name punk::path::relative\
-summary\
"Compute the relative path from a reference path to a location path."\
-help\
"Taking two directory paths, a reference and a location, computes the path
of the location relative to the reference.
Will return a single dot '.' if the paths are the same.
Both paths must be the same type - ie both absolute or both relative.
Matching is case sensitive. On windows, the drive-letter component (only) is
not case sensitive, so punk::path::relative c:/etc C:/etc returns '.'.
The part following the driveletter is case sensitive, so
punk::path::relative c:/etc C:/Etc returns ../Etc.
On windows, if the paths are absolute and specify different volumes,
only the location will be returned."
@leaders -min 2 -max 2
reference -type string -help\
"The path from which the relative path to location is determined."
location -type string -help\
"The location path which may be above or below the reference path."
}
#maint warning - also in punkcheck
proc relative {reference location} {
#*** !doctools
#[call [fun relative] [arg reference] [arg location]]
#[para] Taking two directory paths, a reference and a location, computes the path
# of the location relative to the reference.
#[list_begin itemized]
#[item]
#[para] Arguments:
# [list_begin arguments]
# [arg_def string reference] The path from which the relative path to location is determined.
# [arg_def string location] The location path which may be above or below the reference path
# [list_end]
#[item]
#[para] Results:
#[para] The relative path of the location to the reference path.
#[para] Will return a single dot "." if the paths are the same
#[item]
#[para] Notes:
#[para] Both paths must be the same type - ie both absolute or both relative
#[para] Case sensitive. ie punk::path::relative /etc /etC
# will return ../etC
#[para] On windows, the drive-letter component (only) is not case sensitive
#[example_begin]
# P% punk::path::relative c:/etc C:/etc
# - .
#[example_end]
#[para] The part following the driveletter is case sensitive so in the following cases it recognises the driveletter matches but not the tail
#[example_begin]
# P% punk::path::relative c:/etc C:/Etc
# - ../Etc
#[example_end]
#[para] On windows, if the paths are absolute and specifiy different volumes, only the location will be returned.
#[example_begin]
# P% punk::path::relative c:/etc d:/etc/blah
# - d:/etc/blah
#[example_end]
#[para] Unix-like examples:
#[example_begin]
# P% punk::path::relative /usr/local/etc/ /usr/local/etc/somewhere/below
# - somewhere/below
# P% punk::path::relative /usr/local/etc/somewhere /usr/local/lib/here
# - ../../lib/here
#[example_end]
#[list_end]
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).

1611
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.2.0.tm

File diff suppressed because it is too large Load Diff

2479
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.2.0.tm

File diff suppressed because it is too large Load Diff

178
src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl

@ -282,6 +282,52 @@ namespace eval ::punkboot::lib {
}
#------------------------------------------------------------------------------
# Bootsupport staleness helpers self-contained Tcl, no punk package deps.
# These run in the early boot phase before punk packages are guaranteed loaded
# (modeled on punk::lib::askuser but without punk::console / raw-mode deps).
#------------------------------------------------------------------------------
# Classify the version delta between an old (bootsupport) and new (source)
# semver as "major", "minor", or "patch".
# Caller must verify new_ver > old_ver via `package vcompare` first.
proc ::punkboot::lib::bootsupport_bump_level {old_ver new_ver} {
set op [split $old_ver .]
set np [split $new_ver .]
while {[llength $op] < 3} {lappend op 0}
while {[llength $np] < 3} {lappend np 0}
set op [lmap s $op {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
set np [lmap s $np {
set n [scan $s %d]
expr {[string is integer -strict $n] ? $n : 0}
}]
lassign $op o_major o_minor o_patch
lassign $np n_major n_minor n_patch
if {$n_major > $o_major} {return "major"}
if {$n_minor > $o_minor} {return "minor"}
return "patch"
}
# Self-contained y/N prompt. Returns 1 for y/yes (case-insensitive), 0 otherwise.
# Used by the staleness abort path to ask the user whether to proceed past a
# minor-version bump. Default is No (Enter = abort) so accidental Enter is safe.
proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
puts stdout $prompt
puts -nonewline stdout "Proceed anyway? (y/N) "
flush stdout
set stdin_state [chan configure stdin]
try {
chan configure stdin -blocking 1
set answer [string tolower [string trim [gets stdin]]]
} finally {
chan configure stdin -blocking [dict get $stdin_state -blocking]
}
return [expr {$answer eq "y" || $answer eq "yes"}]
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -469,6 +515,52 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} {
#package forget punk::repo
#package forget punkcheck
#------------------------------------------------------------------
# Bootsupport staleness check
# When make.tcl is run from project root (not from src/), bootsupport
# modules are loaded in preference to source modules. If the source
# modules have been updated (e.g API changes) but bootsupport hasn't
# been rebuilt, make.tcl will fail with obscure errors from the stale
# bootsupport versions. This check compares buildversion.txt in source
# against the highest .tm version in bootsupport and warns the caller.
#------------------------------------------------------------------
if {[file tail $startdir] ne "src"} {
set _runtime_deps [list {*}{
} punkcheck punkcheck-buildversion.txt punkcheck-*.tm {*}{
} punk::repo punk/repo-buildversion.txt punk/repo-*.tm {*}{
} punk::mix punk/mix-buildversion.txt punk/mix-*.tm {*}{
} punk::tdl punk/tdl-buildversion.txt punk/tdl-*.tm {*}{
}]
set _stale_modules [list]
foreach {pkg buildversion_rel bootsupport_glob} $_runtime_deps {
set _src_bv_file [file join $::punkboot::scriptfolder modules $buildversion_rel]
set _bs_glob_dir [file dirname [file join $::punkboot::scriptfolder bootsupport modules $bootsupport_glob]]
set _bs_glob_pat [file tail $bootsupport_glob]
if {![file exists $_src_bv_file]} {continue}
#read source version (first line of buildversion.txt)
set _fd [open $_src_bv_file r]
set _src_version [string trim [lindex [split [read $_fd] \n] 0]]
close $_fd
#find highest version .tm in bootsupport
set _bs_version ""
set _prefix [string range $_bs_glob_pat 0 [string first "*" $_bs_glob_pat]-1]
foreach _tm [glob -nocomplain -dir $_bs_glob_dir -- $_bs_glob_pat] {
set _v [file rootname [string range [file tail $_tm] [string length $_prefix] end]]
if {$_bs_version eq "" || [package vcompare $_v $_bs_version] == 1} {
set _bs_version $_v
}
}
if {$_bs_version ne "" && $_src_version ne "" && [package vcompare $_src_version $_bs_version] == 1} {
set _bump [::punkboot::lib::bootsupport_bump_level $_bs_version $_src_version]
lappend _stale_modules [list $pkg $_src_version $_bs_version $_bump]
}
}
if {[llength $_stale_modules]} {
set ::punkboot::stale_bootsupport $_stale_modules
}
unset _runtime_deps _stale_modules
}
package require punk::repo ;#todo - push our requirements to a smaller punk::repo::xxx package with minimal dependencies
package require punk::mix
package require punkcheck
@ -1433,6 +1525,51 @@ if {[lsearch $::argv -k] >= 0} {
#puts stdout "::argv $::argv"
# ----------------------------------------
# Abort / prompt if bootsupport modules are stale relative to source.
# Behaviour is version-aware based on the bump level between each stale
# module's bootsupport version and source version:
# - major bump -> abort unconditionally (breaking change, old bootsupport will not work)
# - minor bump -> prompt user; backward-compat aliases *should* keep old bootsupport functional
# - patch bump -> warn and proceed (non-breaking)
# 'check' is exempt in all cases the warning is shown at the end of its output instead.
if {[info exists ::punkboot::stale_bootsupport] && $::punkboot::command ne "check"} {
set _stale $::punkboot::stale_bootsupport
set _have_major 0
set _have_minor 0
set _have_patch 0
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
switch -- $_bump {
major {incr _have_major}
minor {incr _have_minor}
patch {incr _have_patch}
default {incr _have_patch}
}
}
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $_stale {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Recommended fix update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
if {$_have_major} {
puts stderr "-aborted- (major version bump in stale bootsupport breaking change, cannot proceed)"
exit 1
}
if {$_have_minor} {
if {![::punkboot::lib::bootsupport_prompt_yesno "Minor-version staleness backward-compat aliases should keep old bootsupport functional."]} {
puts stderr "-aborted- by user"
exit 1
}
puts stderr "-proceeding with stale bootsupport at user's request-"
}
# patch-only staleness: warn and proceed (warning already printed above)
}
set scriptfolder $::punkboot::scriptfolder
@ -1582,6 +1719,19 @@ if {$::punkboot::command eq "check"} {
puts stdout $sep
catch {package require struct::set}
puts stdout ---[package ifneeded struct::set 2.2.3]
# Show bootsupport staleness warning at the end of check output
if {[info exists ::punkboot::stale_bootsupport]} {
puts stderr "=============================================================================="
puts stderr "WARNING: bootsupport modules are stale relative to source modules:"
foreach _entry $::punkboot::stale_bootsupport {
lassign $_entry _pkg _src_ver _bs_ver _bump
puts stderr [format " %-15s bootsupport=%-10s source=%-10s bump=%s" $_pkg $_bs_ver $_src_ver $_bump]
}
puts stderr ""
puts stderr " Run the following to update bootsupport before proceeding:"
puts stderr " cd \[projectroot\]/src && tclsh make.tcl modules && tclsh make.tcl bootsupport"
puts stderr "=============================================================================="
}
exit 0
}
@ -2025,7 +2175,7 @@ if {$::punkboot::command eq "bootsupport"} {
set resultdict [punkcheck::install $sourcemodules $targetroot\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-installer "punkboot-bootsupport"\
-progresschannel stdout\
]
@ -2042,7 +2192,7 @@ if {$::punkboot::command eq "bootsupport"} {
puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $source_bootscript to $targetroot_bootscript (if source file changed)"
set resultdict [punkcheck::install [file dirname $source_bootscript] $targetroot_bootscript\
-glob make.tcl\
-max_depth 1\
-max-depth 1\
-createempty 0\
-overwrite installedsourcechanged-targets\
-installer "punkboot-bootsupport"\
@ -2098,11 +2248,11 @@ if {$::punkboot::command in {project packages modules}} {
-overwrite installedsourcechanged-targets
-progresschannel stdout
-exclude-filetails {AGENTS.md include_modules.config}
-antiglob_paths {README.md}
-exclude-paths {README.md}
}]
# -antiglob_paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
# -exclude-paths {README.md AGENTS.md **/AGENTS.md include_modules.config}
#-exclude-filetails {AGENTS.md include_modules.config}
#-antiglob_paths {README.md}
#-exclude-paths {README.md}
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
}
if {![llength $vendormodulefolders]} {
@ -2112,7 +2262,7 @@ if {$::punkboot::command in {project packages modules}} {
if {$::punkboot::command in {project packages libs}} {
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2130,7 +2280,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB: copying tcl-version neutral and platform neutral libraries from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2150,7 +2300,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2165,7 +2315,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "VENDORLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2183,7 +2333,7 @@ if {$::punkboot::command in {project packages libs}} {
########################################################
lappend projectlibfolders lib
#exclude README.md from source folder - but only the root one
#-antiglob_paths takes relative patterns e.g
#-exclude-paths takes relative patterns e.g
# */test.txt will only match test.txt exactly one level deep.
# */*/*.foo will match any path ending in .foo that is exactly 2 levels deep.
# **/test.txt will match at any level below the root (but not in the root)
@ -2200,7 +2350,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB: copying from $sourcefolder/$lf to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $sourcefolder/$lf $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2219,7 +2369,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2235,7 +2385,7 @@ if {$::punkboot::command in {project packages libs}} {
puts stdout "PROJECTLIB_$which: copying from $source_lib_folder to $target_lib_folder (if source file changed)"
set resultdict [punkcheck::install $source_lib_folder $target_lib_folder\
-overwrite installedsourcechanged-targets\
-antiglob_paths $antipaths\
-exclude-paths $antipaths\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]
@ -2358,7 +2508,7 @@ if {$::punkboot::command in {project packages modules}} {
set resultdict [punkcheck::install_non_tm_files $src_module_dir $target_modules_base\
-installer make.tcl\
-overwrite $overwrite\
-antiglob_paths {README.md **/AGENTS.md AGENTS.md}\
-exclude-paths {README.md **/AGENTS.md AGENTS.md}\
-progresschannel stdout\
]
puts stdout [punkcheck::summarize_install_resultdict $resultdict]

2
src/tests/modules/AGENTS.md

@ -37,3 +37,5 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
## Child DOX Index
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict)

94
src/tests/modules/punk/path/testsuites/tests/path.test

@ -517,7 +517,7 @@ namespace eval ::testspace {
-cleanup $treefilenames_cleanup \
-result 1
test treefilenames_rejects_internal_call_flags {Internal recursion state flags are not public API} \
test treefilenames_rejects_internal_call_flags {Internal recursion state flags are not public API} \
-setup $treefilenames_tree -body {
set results [list]
foreach arglist {
@ -531,6 +531,96 @@ namespace eval ::testspace {
} \
-cleanup $treefilenames_cleanup \
-result 1
}
# =========================================================================
# --- treefilenames: hidden files and hidden directories ---
# On Windows, "hidden" means the hidden file attribute is set.
# On Unix, "hidden" means the filename starts with a dot (.).
# Dotfiles/dot-directories are NOT hidden on Windows — they are found by
# regular glob -type f. The hidden attribute is Windows-specific.
# These tests use platform constraints to test the relevant behaviour.
# =========================================================================
# Constraint: windows hidden attribute
tcltest::testConstraint winHidden [expr {$::tcl_platform(platform) eq "windows"}]
# Constraint: unix dotfiles (files starting with . are hidden by convention)
tcltest::testConstraint unixDotfiles [expr {$::tcl_platform(platform) eq "unix"}]
variable treefilenames_hidden_tree {
set tfh_prevdir [pwd]
set tfh_newbase [punk::lib::tempdir_newfolder -prefix punk_path_tf_hidden]
set tfh_tree_root [file join $tfh_newbase __punk_path_tf_hidden_test__]
file mkdir $tfh_tree_root
file mkdir [file join $tfh_tree_root regulardir]
file mkdir [file join $tfh_tree_root .dotdir]
#regular files
set fd [open [file join $tfh_tree_root regular.txt] w]; close $fd
set fd [open [file join $tfh_tree_root regulardir/inside.txt] w]; close $fd
#dotfile (hidden on unix, regular on windows)
set fd [open [file join $tfh_tree_root .dotfile] w]; close $fd
set fd [open [file join $tfh_tree_root .dotdir/inside_dotdir.txt] w]; close $fd
#windows hidden attribute files
set tfh_wh_file [file join $tfh_tree_root win_hidden.txt]
set fd [open $tfh_wh_file w]; close $fd
set tfh_wh_dir [file join $tfh_tree_root win_hiddendir]
file mkdir $tfh_wh_dir
set fd [open [file join $tfh_wh_dir/inside.txt] w]; close $fd
if {$::tcl_platform(platform) eq "windows"} {
file attributes $tfh_wh_file -hidden 1
file attributes $tfh_wh_dir -hidden 1
}
}
variable treefilenames_hidden_cleanup {
cd $tfh_prevdir
file delete -force $tfh_newbase
}
test treefilenames_finds_all_files_including_hidden {treefilenames finds hidden files and files inside hidden directories} \
-setup $treefilenames_hidden_tree -body {
set result [lsort [punk::path::treefilenames -sort none -directory $tfh_tree_root -tailbase $tfh_tree_root *]]
}\
-cleanup $treefilenames_hidden_cleanup \
-result {.dotdir/inside_dotdir.txt .dotfile regular.txt regulardir/inside.txt win_hidden.txt win_hiddendir/inside.txt}
test treefilenames_finds_files_in_hidden_dirs {treefilenames recurses into hidden directories to find files within}\
-constraints winHidden \
-setup $treefilenames_hidden_tree -body {
set result [lsort [punk::path::treefilenames -sort none -directory $tfh_tree_root -tailbase $tfh_tree_root *]]
#win_hiddendir has the hidden attribute set on windows
expr {"win_hiddendir/inside.txt" in $result}
}\
-cleanup $treefilenames_hidden_cleanup \
-result 1
test treefilenames_finds_hidden_attribute_files {treefilenames finds files with the windows hidden attribute set}\
-constraints winHidden \
-setup $treefilenames_hidden_tree -body {
set result [lsort [punk::path::treefilenames -sort none -directory $tfh_tree_root -tailbase $tfh_tree_root *]]
#win_hidden.txt has the hidden attribute set on windows
expr {"win_hidden.txt" in $result}
}\
-cleanup $treefilenames_hidden_cleanup \
-result 1
test treefilenames_finds_dotfiles {treefilenames finds dotfiles (files starting with .)}\
-setup $treefilenames_hidden_tree -body {
set result [lsort [punk::path::treefilenames -sort none -directory $tfh_tree_root -tailbase $tfh_tree_root *]]
#dotfile should be found on all platforms
#on windows it's a regular file, on unix it's a hidden file
expr {".dotfile" in $result}
}\
-cleanup $treefilenames_hidden_cleanup \
-result 1
test treefilenames_finds_files_in_dotdirs {treefilenames recurses into dot-directories to find files within}\
-setup $treefilenames_hidden_tree -body {
set result [lsort [punk::path::treefilenames -sort none -directory $tfh_tree_root -tailbase $tfh_tree_root *]]
#.dotdir should be recursed into on all platforms
#on windows it's a regular directory, on unix it's a hidden directory
expr {".dotdir/inside_dotdir.txt" in $result}
}\
-cleanup $treefilenames_hidden_cleanup \
-result 1
}
tcltest::cleanupTests ;#needed to produce test summary.

684
src/tests/modules/punkcheck/testsuites/punkcheck/install.test

@ -0,0 +1,684 @@
# -*- tcl -*-
# Characterization tests for punkcheck::install and punkcheck::summarize_install_resultdict
# These tests lock down CURRENT behavior with current flag names (underscores) before any refactoring.
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkcheck/** install.test
package require tcltest
package require punkcheck
package require punk::lib
package require punk::path
namespace eval ::testspace {
namespace import ::tcltest::*
# -------------------------------------------------------------------------
# Helper: create a unique temp workspace with a source tree layout.
# Returns a dict with keys: root srcdir tgtdir
# Caller is responsible for cleanup via -cleanup {file delete -force $root}
# -------------------------------------------------------------------------
proc make_workspace {layout} {
set root [punk::lib::tempdir_newfolder -prefix punkchecktest]
set srcdir [file join $root src]
set tgtdir [file join $root tgt]
file mkdir $srcdir $tgtdir
foreach {relpath content} $layout {
set f [file join $srcdir {*}[file split $relpath]]
file mkdir [file dirname $f]
set fd [open $f w]
chan configure $fd -translation lf
puts -nonewline $fd $content
close $fd
}
return [dict create root $root srcdir $srcdir tgtdir $tgtdir]
}
# =========================================================================
# --- punkcheck::install: basic copy behavior ---
# =========================================================================
test install_basic_copy {Basic copy of a single file to empty target}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir] -overwrite all-targets -createdir 1]
set copied [dict get $resultdict files_copied]
set got [string map [list [dict get $ws srcdir]/ ""] [lindex $copied 0]]
return $got
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {foo.txt}
test install_missing_target_no_createdir {Error when target missing and -createdir 0}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
# tgtdir was created by make_workspace but we remove it to test the error path
file delete -force [dict get $ws tgtdir]
punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir] -overwrite all-targets
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-returnCodes error\
-match glob\
-result {punkcheck::install base target dir*doesn't exist*}
test install_createdir_creates_target {Target dir is created when -createdir 1 and missing}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
file delete -force [dict get $ws tgtdir]
punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir] -overwrite all-targets -createdir 1
file exists [dict get $ws tgtdir]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- punkcheck::install: -overwrite modes ---
# =========================================================================
test install_no_targets_only_copies_missing {no-targets copies only files missing at target}\
-body {
set ws [make_workspace [list a.txt "aaa" b.txt "bbb"]]
# Pre-create a.txt at target with different content
set fd [open [file join [dict get $ws tgtdir] a.txt] w]
puts -nonewline $fd "OLD_CONTENT"
close $fd
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir] -overwrite no-targets]
set copied_basenames [list]
foreach f [dict get $resultdict files_copied] {
lappend copied_basenames [file tail $f]
}
return $copied_basenames
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {b.txt}
test install_all_targets_overwrites_existing {all-targets overwrites existing files}\
-body {
set ws [make_workspace [list a.txt "new"]]
set fd [open [file join [dict get $ws tgtdir] a.txt] w]
puts -nonewline $fd "old"
close $fd
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir] -overwrite all-targets]
set fd [open [file join [dict get $ws tgtdir] a.txt] r]
set content [read -nonewline $fd]
close $fd
return $content
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {new}
# =========================================================================
# --- punkcheck::install: -exclude-filetails ---
# =========================================================================
test install_exclude_filetails {Excluded file tails are not copied}\
-body {
set ws [make_workspace [list keep.txt "k" skip.bad "x"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-filetails [list "*.bad"]]
set copied_basenames [list]
foreach f [dict get $resultdict files_copied] {
lappend copied_basenames [file tail $f]
}
return $copied_basenames
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt}
# =========================================================================
# --- punkcheck::install: -exclude-dirsegments ---
# =========================================================================
test install_exclude_dirsegments {Excluded dir segments are not descended into}\
-body {
set ws [make_workspace [list keep.txt "k" _aside/secret.txt "s"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-dirsegments [list "_aside"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return $all_copied
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt}
# =========================================================================
# --- punkcheck::install: -antiglob_paths ---
# =========================================================================
test install_antiglob_paths {antiglob_paths excludes matching relative paths}\
-body {
set ws [make_workspace [list keep.txt "k" blocked/secret.txt "s"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-antiglob_paths [list "blocked/**"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return $all_copied
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt}
# -------------------------------------------------------------------------
# Additional characterization tests to lock behaviour before merge of
# -exclude-dirsegments + -antiglob_paths into unified -exclude-paths.
# These tests verify: nested dir-seg match, _core defaults applied by
# default, _core override REPLACES (not appends), antiglob top-level
# filename, antiglob deep **/FILE, and combined mechanism.
# -------------------------------------------------------------------------
test install_exclude_dirsegments_nested {dir-seg exclusion applies at any directory level, not just top}\
-body {
set ws [make_workspace [list keep.txt "k" parent/_aside/secret.txt "s" parent/keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-dirsegments [list "_aside"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt parent/keep2.txt}
test install_exclude_dirsegments_core_default_applied {default _core excludes _build and .git at any level}\
-body {
# Without explicit -exclude-dirsegments_core, default core {#* _aside _build .git .fossil*} is applied
set ws [make_workspace [list\
keep.txt "k"\
_build/intermediate.txt "i"\
parent/_build/deep.txt "d"\
.git/config "c"\
keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-dirsegments [list]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt keep2.txt}
test install_exclude_dirsegments_core_override_replaces {passing -exclude-dirsegments_core REPLACES the defaults, not appends}\
-body {
# Override core with {#* _aside .git} - removes .fossil* AND _build exclusion
# so .fossil-custom IS copied, _build IS copied, .git IS still excluded
set ws [make_workspace [list\
keep.txt "k"\
_build/intermediate.txt "i"\
.fossil-custom/menu "m"\
.git/config "c"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-dirsegments_core [list "#*" "_aside" ".git"]\
-exclude-dirsegments [list]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {.fossil-custom/menu _build/intermediate.txt keep.txt}
test install_antiglob_paths_toplevel_filename {antiglob_paths with a bare filename excludes top-level file}\
-body {
# bare filename (no path separators) - used by src/make.tcl call sites
set ws [make_workspace [list keep.txt "k" README.md "r" other.txt "o"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-antiglob_paths [list "README.md"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt other.txt}
test install_antiglob_paths_deep_doublestar {antiglob_paths **/FILE matches nested files but NOT top-level (no leading slash)}\
-body {
# **/AGENTS.md only matches nested AGENTS.md (those with a parent path segment)
# Top-level AGENTS.md requires the bare 'AGENTS.md' pattern to be excluded too.
# This is the actual globmatchpath semantics - confirmed by make.tcl using both forms.
set ws [make_workspace [list\
keep.txt "k"\
AGENTS.md "a1"\
sub/AGENTS.md "a2"\
sub/deep/AGENTS.md "a3"\
sub/deep/keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-antiglob_paths [list "**/AGENTS.md"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {AGENTS.md keep.txt sub/deep/keep2.txt}
test install_antiglob_paths_toplevel_and_deep {combining bare filename AND **/FILE excludes all occurrences}\
-body {
# {AGENTS.md **/AGENTS.md} - covers both top-level and nested
set ws [make_workspace [list\
keep.txt "k"\
AGENTS.md "a1"\
sub/AGENTS.md "a2"\
sub/deep/AGENTS.md "a3"\
sub/deep/keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-antiglob_paths [list "AGENTS.md" "**/AGENTS.md"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt sub/deep/keep2.txt}
test install_antiglob_paths_dir_skip {antiglob_paths with pattern matching a directory path skips that dir's recursion}\
-body {
# **/_aside excludes _aside at any level as a directory (skip recursion)
set ws [make_workspace [list\
keep.txt "k"\
_aside/secret.txt "s1"\
parent/_aside/secret2.txt "s2"\
parent/keep3.txt "k3"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-antiglob_paths [list "**/_aside" "_aside"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt parent/keep3.txt}
# =========================================================================
# --- punkcheck::install: -exclude-paths (unified mechanism) ---
# These tests verify the new -exclude-paths flag directly.
# The legacy flags above (-exclude-dirsegments, -antiglob_paths) are
# converted internally to -exclude-paths and must produce the same results.
# =========================================================================
test install_exclude_paths_basic {exclude-paths with simple top-level file pattern excludes that file}\
-body {
set ws [make_workspace [list keep.txt "k" skip.me "s" other.txt "o"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-paths [list "skip.me"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt other.txt}
test install_exclude_paths_dir_skip {exclude-paths with **/dir pattern skips that dir at any level}\
-body {
set ws [make_workspace [list\
keep.txt "k"\
_aside/secret.txt "s1"\
parent/_aside/secret2.txt "s2"\
parent/keep3.txt "k3"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-paths [list "**/_aside" "_aside"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt parent/keep3.txt}
test install_exclude_paths_deep_doublestar {exclude-paths **/FILE matches nested files but not top-level}\
-body {
set ws [make_workspace [list\
keep.txt "k"\
AGENTS.md "a1"\
sub/AGENTS.md "a2"\
sub/deep/AGENTS.md "a3"\
sub/deep/keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-paths [list "**/AGENTS.md"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {AGENTS.md keep.txt sub/deep/keep2.txt}
test install_exclude_paths_combined {exclude-paths with both bare and **/ forms excludes all occurrences}\
-body {
set ws [make_workspace [list\
keep.txt "k"\
AGENTS.md "a1"\
sub/AGENTS.md "a2"\
sub/deep/AGENTS.md "a3"\
sub/deep/keep2.txt "k2"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-paths [list "AGENTS.md" "**/AGENTS.md"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {keep.txt sub/deep/keep2.txt}
test install_exclude_paths_core_override {exclude-paths_core override REPLACES default core}\
-body {
# Override core with only #* and _aside forms - removes .git and .fossil* exclusion
set ws [make_workspace [list\
keep.txt "k"\
_aside/secret.txt "s"\
.git/config "c"\
.fossil-custom/menu "m"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-exclude-paths_core [list "#*" "**/#*" "_aside" "**/_aside"]]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {.fossil-custom/menu .git/config keep.txt}
# =========================================================================
# --- punkcheck::install: hyphenated flag names (Phase 5 rename) ---
# These tests verify that the new hyphenated flag names work directly.
# The old underscore forms are still accepted via backward-compat aliases.
# =========================================================================
test install_max_depth_hyphenated {-max-depth (hyphenated) limits recursion depth}\
-body {
set ws [make_workspace [list top.txt "t" sub/deep.txt "d"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-max-depth 0]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {top.txt}
test install_punkcheck_folder_hyphenated {-punkcheck-folder (hyphenated) places .punkcheck in target}\
-body {
set ws [make_workspace [list keep.txt "k"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-punkcheck-folder target]
set punkcheck_exists [file exists [file join [dict get $ws tgtdir] .punkcheck]]
return $punkcheck_exists
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test install_source_checksum_hyphenated {-source-checksum (hyphenated) stores checksums}\
-body {
set ws [make_workspace [list keep.txt "k"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-source-checksum comparestore]
set has_punkcheck [file exists [file join [dict get $ws tgtdir] .punkcheck]]
return $has_punkcheck
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- punkcheck::install: -glob ---
# =========================================================================
test install_glob_pattern {Only files matching glob are copied}\
-body {
set ws [make_workspace [list a.tm "a" b.txt "b"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-glob "*.tm"]
set copied_basenames [list]
foreach f [dict get $resultdict files_copied] {
lappend copied_basenames [file tail $f]
}
return $copied_basenames
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {a.tm}
# =========================================================================
# --- punkcheck::install: -max_depth ---
# =========================================================================
test install_max_depth_limits_recursion {max_depth 0 limits to top-level only}\
-body {
set ws [make_workspace [list top.txt "t" sub/deep.txt "d"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-max_depth 0]
set copied_basenames [list]
foreach f [dict get $resultdict files_copied] {
lappend copied_basenames [string map [list [dict get $ws srcdir]/ ""] $f]
}
return $copied_basenames
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {top.txt}
test install_max_depth_1_allows_one_level {max_depth 1 allows one level of recursion}\
-body {
set ws [make_workspace [list top.txt "t" sub/deep.txt "d"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1\
-max_depth 1]
set all_copied [list]
foreach f [dict get $resultdict files_copied] {
lappend all_copied [string map [list [dict get $ws srcdir]/ ""] $f]
}
return [lsort $all_copied]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {sub/deep.txt top.txt}
# =========================================================================
# --- punkcheck::install: -createempty ---
# =========================================================================
test install_createempty_creates_empty_dirs {createempty creates target dirs with no matching files}\
-body {
set ws [make_workspace [list sub/empty.txt "e"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1 -createempty 1]
set empty_dir [file join [dict get $ws tgtdir] sub]
return [file isdirectory $empty_dir]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- punkcheck::install: resultdict shape ---
# =========================================================================
test install_resultdict_keys {resultdict has expected keys}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1]
set keys [lsort [dict keys $resultdict]]
return $keys
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {antiglob_paths_matched files_copied files_skipped punkcheck_folder punkcheck_records sources_unchanged srcdir tgtdir}
# =========================================================================
# --- punkcheck::summarize_install_resultdict ---
# =========================================================================
test summarize_basic {summarize returns a non-empty string for a resultdict with copies}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1]
set summary [punkcheck::summarize_install_resultdict $resultdict]
return [expr {[string length $summary] > 0}]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test summarize_with_title {summarize includes the title line when -title is set}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1]
set summary [punkcheck::summarize_install_resultdict -title "MY TITLE" $resultdict]
return [regexp {MY TITLE} $summary]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test summarize_no_copies {summarize handles resultdict with zero copied files}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
set resultdict [punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite no-targets]
set summary [punkcheck::summarize_install_resultdict $resultdict]
return [expr {[string length $summary] > 0}]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test summarize_empty_resultdict {summarize returns non-empty string even when no files copied}\
-body {
set rd [dict create files_copied [list] files_skipped [list] sources_unchanged [list] antiglob_paths_matched [list] punkcheck_records [list] punkcheck_folder [pwd] srcdir [pwd] tgtdir [pwd]]
set summary [punkcheck::summarize_install_resultdict $rd]
return [regexp {Copied 0 files} $summary]
}\
-result 1
test summarize_unknown_option {summarize raises error on unknown option}\
-body {
punkcheck::summarize_install_resultdict -bogus 1 [dict create files_copied [list] files_skipped [list] sources_unchanged [list] antiglob_paths_matched [list] punkcheck_records [list] punkcheck_folder [pwd] srcdir [pwd] tgtdir [pwd]]
}\
-returnCodes error\
-match glob\
-result {*}
# =========================================================================
# --- punkcheck::install: -punkcheck_folder ---
# =========================================================================
test install_punkcheck_folder_target {punkcheck_folder=target places .punkcheck in tgtdir}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1 -punkcheck_folder target
file exists [file join [dict get $ws tgtdir] .punkcheck]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test install_punkcheck_folder_source {punkcheck_folder=source places .punkcheck in srcdir}\
-body {
set ws [make_workspace [list foo.txt "hello"]]
punkcheck::install [dict get $ws srcdir] [dict get $ws tgtdir]\
-overwrite all-targets -createdir 1 -punkcheck_folder source
file exists [file join [dict get $ws srcdir] .punkcheck]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
}
tcltest::cleanupTests ;#needed to produce test summary line.

613
src/tests/modules/punkcheck/testsuites/punkcheck/installtrack.test

@ -0,0 +1,613 @@
# -*- tcl -*-
# Characterization tests for punkcheck::installtrack object model API
# These tests lock down the installtrack/installevent OO interface before any refactoring.
# The installtrack class is used directly by punk::mix::cli and punk::mix::commandset modules.
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkcheck/** installtrack.test
package require tcltest
package require punkcheck
package require punk::lib
namespace eval ::testspace {
namespace import ::tcltest::*
# -------------------------------------------------------------------------
# Helper: create a temp workspace with a .punkcheck folder context
# Returns a dict with keys: root punkcheck_file srcdir tgtdir
# -------------------------------------------------------------------------
proc make_workspace {} {
set root [punk::lib::tempdir_newfolder -prefix pktracktest]
set punkcheck_file [file join $root .punkcheck]
set srcdir [file join $root src]
set tgtdir [file join $root tgt]
file mkdir $srcdir $tgtdir
return [dict create root $root punkcheck_file $punkcheck_file srcdir $srcdir tgtdir $tgtdir]
}
# -------------------------------------------------------------------------
# Helper: create a source file inside punkcheck_folder (root) and return abs path
# -------------------------------------------------------------------------
proc make_source_file {root relpath content} {
set fpath [file join $root $relpath]
file mkdir [file dirname $fpath]
set fd [open $fpath w]
try { puts -nonewline $fd $content } finally { close $fd }
return $fpath
}
# -------------------------------------------------------------------------
# Helper: run a full VIRTUAL targetset lifecycle to seed a prior completed
# VIRTUAL-RECORD, so subsequent targetset_source_changes calls compare
# against it. Uses the same installer name "test" and same target name as
# the subsequent test code, so the FILEINFO record matches.
# -------------------------------------------------------------------------
proc seed_virtual_record {punkcheck_file srcdir tgtdir target_name source_abspath} {
set installer [punkcheck::installtrack new "test" $punkcheck_file]
$installer set_source_target $srcdir $tgtdir
set event [$installer start_event {-install_step seed}]
$event targetset_init VIRTUAL $target_name
$event targetset_addsource $source_abspath
$event targetset_started
$event targetset_end OK
$event end
$installer destroy
}
# =========================================================================
# --- installtrack: constructor and basic accessors ---
# =========================================================================
test installtrack_construct {Constructor creates an installtrack object}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test-installer" [dict get $ws punkcheck_file]]
set is_object [expr {[info commands $installer] ne ""}]
$installer destroy
return $is_object
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installtrack_get_name {get_name returns installer name passed to constructor}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "my-installer" [dict get $ws punkcheck_file]]
set name [$installer get_name]
$installer destroy
return $name
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {my-installer}
test installtrack_get_checkfile {get_checkfile returns punkcheck file path}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
set checkfile [$installer get_checkfile]
$installer destroy
return [string map [list [dict get $ws root] ""] $checkfile]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {/.punkcheck}
test installtrack_get_debugchannel_default {get_debugchannel returns empty string by default}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
set debugchannel [$installer get_debugchannel]
$installer destroy
return $debugchannel
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {}
# =========================================================================
# --- installtrack: set_source_target ---
# =========================================================================
test installtrack_set_source_target {set_source_target stores source and target for events}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set record [$event as_record]
$installer destroy
# The event record should contain -source key
return [dict exists $record -source]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installtrack: start_event and installevent object ---
# =========================================================================
test installtrack_start_event_returns_event {start_event returns an installevent object}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set is_object [expr {[info commands $event] ne ""}]
$installer destroy
return $is_object
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installevent_get_id {event get_id returns a non-empty UUID}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set event_id [$event get_id]
$installer destroy
return [expr {[string length $event_id] > 0}]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installevent_as_record {event as_record returns a dict with tag EVENT}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set record [$event as_record]
$installer destroy
return [dict get $record tag]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {EVENT}
test installevent_as_record_keys {event as_record has expected keys}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set record [$event as_record]
$installer destroy
set keys [lsort [dict keys $record]]
return $keys
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {-config -id -source -targets -ts_begin -ts_end -tsiso_begin -tsiso_end -types tag}
# =========================================================================
# --- installtrack: events list and get_event ---
# =========================================================================
test installtrack_events_list {events returns event collection after start_event}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
# events method uses tailcall to delegate to oolib::collection
set count [$installer events count]
$installer destroy
return $count
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installtrack_get_event {get_event retrieves the current event object}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
set retrieved [$installer get_event]
set same [expr {[$event get_id] eq [$retrieved get_id]}]
$installer destroy
return $same
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installtrack: end_event and cleanup ---
# =========================================================================
test installtrack_end_event {end_event does not error after start_event}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$installer end_event
$installer destroy
return 1
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installtrack_destroy {destroy cleans up object without error}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer destroy
return [expr {![info exists installer] || [info commands $installer] eq ""}]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installtrack: save_all_records persists .punkcheck file ---
# =========================================================================
test installtrack_save_all_records {save_all_records creates .punkcheck file on disk}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$installer save_all_records
set exists [file exists [dict get $ws punkcheck_file]]
$installer destroy
return $exists
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installevent: targetset lifecycle ---
# =========================================================================
test installevent_targetset_init {targetset_init accepts operation and targetset name}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init install "myfile.txt"
$installer destroy
return 1
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installevent_targetset_started {targetset_started runs without error after init}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init install "myfile.txt"
$event targetset_started
$installer destroy
return 1
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installevent_targetset_end {targetset_end accepts status argument}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init install "myfile.txt"
$event targetset_started
$event targetset_end ok
$installer destroy
return 1
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test installevent_end {event end finalizes without error}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init install "myfile.txt"
$event targetset_started
$event targetset_end ok
$event end
$installer destroy
return 1
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installtrack: full lifecycle matching punk::mix::cli usage ---
# =========================================================================
test installtrack_full_lifecycle {Full lifecycle matching punk::mix::cli usage pattern}\
-body {
set ws [make_workspace]
set installer [punkcheck::installtrack new "project.new" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step teststep}]
set event_id [$event get_id]
$event targetset_init install "targetfile.txt"
$event targetset_started
$event targetset_end ok
$event end
$installer end_event
$installer save_all_records
set file_exists [file exists [dict get $ws punkcheck_file]]
$installer destroy
return [expr {$file_exists && [string length $event_id] > 0}]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
# =========================================================================
# --- installevent: targetset_source_changes ---
# Used by punk::mix::cli and punk::mix::commandset to decide whether a
# rebuild is needed: source changed => rebuild.
# Return shape is {dict create changed <list> unchanged <list>}.
# A source is "changed" if it has no prior completed record to compare
# against, or its cksum/type/size differs from the prior record.
# =========================================================================
test targetset_source_changes_no_prior_record {First call with no prior completed record reports source as changed}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "content1"]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set result [$event targetset_source_changes]
$event end
$installer destroy
return [list [llength [dict get $result changed]] [llength [dict get $result unchanged]]]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {1 0}
test targetset_source_changes_after_record_unchanged {Re-adding same source after VIRTUAL-RECORD reports unchanged}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "content1"]
seed_virtual_record [dict get $ws punkcheck_file] [dict get $ws srcdir] [dict get $ws tgtdir] "vtarg1" $src_abspath
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set result [$event targetset_source_changes]
$event end
$installer destroy
return [list [llength [dict get $result changed]] [llength [dict get $result unchanged]]]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {0 1}
test targetset_source_changes_after_modified_source {Modifying source content reports changed}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "original"]
seed_virtual_record [dict get $ws punkcheck_file] [dict get $ws srcdir] [dict get $ws tgtdir] "vtarg1" $src_abspath
# modify source content (different size/cksum)
set fd [open $src_abspath w]; puts -nonewline $fd "modified-content"; close $fd
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set result [$event targetset_source_changes]
$event end
$installer destroy
return [list [llength [dict get $result changed]] [llength [dict get $result unchanged]]]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {1 0}
test targetset_source_changes_dict_keys {Return dict contains changed and unchanged keys}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "x"]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set result [$event targetset_source_changes]
$event end
$installer destroy
return [lsort [dict keys $result]]
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {changed unchanged}
# =========================================================================
# --- installevent: get_targets and get_targets_exist ---
# Used together with targetset_source_changes in the rebuild-decision:
# rebuild = (any changed) OR (existing targets < total targets)
# =========================================================================
test installevent_get_targets {get_targets returns sorted targetlist passed to targetset_init}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "x"]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL [list "t2.txt" "t1.txt"]
set targets [$event get_targets]
$event end
$installer destroy
return $targets
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {t1.txt t2.txt}
test installevent_get_targets_exist_none {get_targets_exist returns empty when no targets exist on disk}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL [list "missing1.txt"]
set existing [$event get_targets_exist]
$event end
$installer destroy
return $existing
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {}
test installevent_get_targets_exist_partial {get_targets_exist returns only existing targets}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
# create one of the two targets at $root (which is the punkcheck_folder)
set fd [open [file join $root "exists.txt"] w]; close $fd
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL [list "missing.txt" "exists.txt"]
set existing [$event get_targets_exist]
$event end
$installer destroy
return $existing
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result {exists.txt}
# =========================================================================
# --- installevent: combined rebuild-decision matching punk::mix::cli ---
# =========================================================================
test targetset_combined_rebuild_changed_source {cli-style: changed source => rebuild needed}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "x"]
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set needs_rebuild [expr {
[llength [dict get [$event targetset_source_changes] changed]]
|| [llength [$event get_targets_exist]] < [llength [$event get_targets]]
}]
$event end
$installer destroy
return $needs_rebuild
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test targetset_combined_rebuild_missing_target {cli-style: unchanged source but missing target => rebuild needed}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "x"]
seed_virtual_record [dict get $ws punkcheck_file] [dict get $ws srcdir] [dict get $ws tgtdir] "vtarg1" $src_abspath
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set needs_rebuild [expr {
[llength [dict get [$event targetset_source_changes] changed]]
|| [llength [$event get_targets_exist]] < [llength [$event get_targets]]
}]
$event end
$installer destroy
return $needs_rebuild
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 1
test targetset_combined_no_rebuild {cli-style: unchanged source + existing target => no rebuild}\
-body {
set ws [make_workspace]
set root [dict get $ws root]
set src_abspath [make_source_file $root "src1.txt" "x"]
seed_virtual_record [dict get $ws punkcheck_file] [dict get $ws srcdir] [dict get $ws tgtdir] "vtarg1" $src_abspath
# create the target file (same name as targetset) so get_targets_exist returns it
set fd [open [file join $root "vtarg1"] w]; close $fd
set installer [punkcheck::installtrack new "test" [dict get $ws punkcheck_file]]
$installer set_source_target [dict get $ws srcdir] [dict get $ws tgtdir]
set event [$installer start_event {-install_step test}]
$event targetset_init VIRTUAL "vtarg1"
$event targetset_addsource $src_abspath
set needs_rebuild [expr {
[llength [dict get [$event targetset_source_changes] changed]]
|| [llength [$event get_targets_exist]] < [llength [$event get_targets]]
}]
$event end
$installer destroy
return $needs_rebuild
}\
-cleanup {
file delete -force [dict get $ws root]
}\
-result 0
}
tcltest::cleanupTests ;#needed to produce test summary line.

1
src/tests/runtests.tcl

@ -34,6 +34,7 @@ if {$libdir_arch ni $::auto_path} {
#------------------------------------
package require punk
package require punk::args
package require Thread
package require shellrun
package require punk::tcltestrun

518
src/vfs/_vfscommon.vfs/modules/commandstack-0.3.tm

@ -1,518 +0,0 @@
#JMN 2021 - Public Domain
#cooperative command renaming
#
# REVIEW 2024 - code was originally for specific use in packageTrace
# - code should be reviewed for more generic utility.
# - API is obscure and undocumented.
# - unclear if intention was only for builtins
# - consider use of newer 'info cmdtype' - (but need also support for safe interps)
# - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack.
# - document that replacement command should use 'commandstack::get_next_command <cmd> <renamer>' for delegating to command as it was prior to rename
#changes:
#2024
# - mungecommand to support namespaced commands
# - fix mistake - hardcoded _originalcommand_package -> _originalcommand_<mungedcommand>
#2021-09-18
# - initial version
# - e.g Support cooperation between packageSuppress and packageTrace which both rename the package command
# - They need to be able to load and unload in any order.
#
#strive for no other package dependencies here.
namespace eval commandstack {
variable all_stacks
variable debug
set debug 0
variable known_renamers [list ::packagetrace ::packageSuppress]
if {![info exists all_stacks]} {
#don't wipe it
set all_stacks [dict create]
}
}
namespace eval commandstack::util {
#note - we can't use something like md5 to ID proc body text because we don't want to require additional packages.
#We could store the full text of the body to compare - but we need to identify magic strings from cooperating packages such as packageTrace
#A magic comment was chosen as the identifying method.
#The string IMPLEMENTOR_*! is searched for where the text between _ and ! is the name of the package that implemented the proc.
#return unspecified if the command is a proc with a body but no magic comment ID
#return unknown if the command doesn't have a proc body to analyze
#otherwise return the package name identified in the magic comment
proc get_IMPLEMENTOR {command} {
#assert - command has already been resolved to a namespace ie fully qualified
if {[llength [info procs $command]]} {
#look for *IMPLEMENTOR_*!
set prefix IMPLEMENTOR_
set suffix "!"
set body [uplevel 1 [list info body $command]]
if {[string match "*$prefix*$suffix*" $body]} {
set prefixposn [string first "$prefix" $body]
set pkgposn [expr {$prefixposn + [string length $prefix]}]
#set suffixposn [string first $suffix [string range $body $pkgposn $pkgposn+60]]
set suffixposn [string first $suffix $body $pkgposn]
return [string range $body $pkgposn $suffixposn-1]
} else {
return unspecified
}
} else {
if {[info commands tcl::info::cmdtype] ne ""} {
#tcl9 and maybe some tcl 8.7s ?
switch -- [tcl::info::cmdtype $command] {
native {
return builtin
}
default {
return undetermined
}
}
} else {
return undetermined
}
}
}
}
namespace eval commandstack::renamed_commands {}
namespace eval commandstack::temp {} ;#where we create proc initially before renaming into place
namespace eval commandstack {
namespace export {[a-z]*}
proc help {} {
return {
}
}
proc debug {{on_off {}}} {
variable debug
if {$on_off eq ""} {
return $debug
} else {
if {[string is boolean -strict $debug]} {
set debug [expr {$on_off && 1}]
return $debug
}
}
}
proc get_stack {{command ""}} {
variable all_stacks
if {$command eq ""} {
return $all_stacks
}
set command [uplevel 1 [list namespace which $command]]
if {[dict exists $all_stacks $command]} {
return [dict get $all_stacks $command]
} else {
return [list]
}
}
#get the implementation to which the renamer (renamer is usually calling namespace) originally renamed it, or the implementation it now points to.
#review - performance impact. Possible to use oo for faster dispatch whilst allowing stack re-orgs?
#e.g if renaming builtin 'package' - this command is generally called 'a lot'
proc get_next_command {command renamer tokenid} {
variable all_stacks
if {[dict exists $all_stacks $command]} {
set stack [dict get $all_stacks $command]
#stack is a list of dicts, 1st entry is token {<cmd> <renamer> <tokenid>}
set posn [lsearch -index 1 $stack [list $command $renamer $tokenid]]
if {$posn > -1} {
set record [lindex $stack $posn]
return [dict get $record implementation]
} else {
error "(commandstack::get_next_command) ERROR: unable to determine next command for '$command' using token: $command $renamer $tokenid"
}
} else {
return $command
}
}
proc basecall {command args} {
variable all_stacks
set command [uplevel 1 [list namespace which $command]]
if {[dict exists $all_stacks $command]} {
set stack [dict get $all_stacks $command]
if {[llength $stack]} {
set rec1 [lindex $stack 0]
tailcall [dict get $rec1 implementation] {*}$args
} else {
tailcall $command {*}$args
}
} else {
tailcall $command {*}$args
}
}
#review.
#<renamer> defaults to calling namespace - but can be arbitrary string
proc rename_command {args} {
#todo: consider -forcebase 1 or similar to allow this rename to point to bottom of stack (original command) bypassing existing renames
# - need to consider that upon removing, that any remaining rename that was higher on the stack should not also be diverted to the base - but rather to the next lower in the stack
#
if {[lindex $args 0] eq "-renamer"} {
set renamer [lindex $args 1]
set arglist [lrange $args 2 end]
} else {
set renamer ""
set arglist $args
}
if {[llength $arglist] != 3} {
error "commandstack::rename_command usage: rename_command ?-renamer <string>? command procargs procbody"
}
lassign $arglist command procargs procbody
set command [uplevel 1 [list namespace which $command]]
set mungedcommand [string map {:: _ns_} $command]
set mungedrenamer [string map {:: _ns_} $renamer]
variable all_stacks
variable known_renamers
variable renamer_command_tokens ;#monotonically increasing int per <mungedrenamer>::<mungedcommand> representing number of renames ever done.
if {$renamer eq ""} {
set renamer [uplevel 1 [list namespace current]]
}
if {$renamer ni $known_renamers} {
lappend known_renamers $renamer
dict set renamer_command_tokens [list $renamer $command] 0
}
#TODO - reduce emissions to stderr - flag for debug?
#e.g packageTrace and packageSuppress packages use this convention.
set nextinfo [uplevel 1 [list\
apply {{command renamer procbody} {
#todo - munge dash so we can make names in renamed_commands separable
# {- _dash_} ?
set mungedcommand [string map {:: _ns_} $command]
set mungedrenamer [string map {:: _ns_} $renamer]
set tokenid [lindex [dict incr renamer_command_tokens [list $renamer $command]] 1]
set next_target ::commandstack::renamed_commands::${mungedcommand}-original-$mungedrenamer-$tokenid ;#default is to assume we are the only one playing around with it, but we'll check for known associates too.
set do_rename 0
if {[llength [info procs $command]] || [llength [info commands $next_target]]} {
#$command is not the standard builtin - something has replaced it, could be ourself.
set next_implementor [::commandstack::util::get_IMPLEMENTOR $command]
set munged_next_implementor [string map {:: _ns_} $next_implementor]
#if undetermined/unspecified it could be the latest renamer on the stack - but we can't know for sure something else didn't rename it.
if {[dict exists $::commandstack::all_stacks $command]} {
set comstacks [dict get $::commandstack::all_stacks $command]
} else {
set comstacks [list]
}
set this_renamer_previous_entries [lsearch -all -index 3 $comstacks $renamer] ;#index 3 is value for second dict entry - (value for key 'renamer')
if {[llength $this_renamer_previous_entries]} {
if {$next_implementor eq $renamer} {
#previous renamer was us. Rather than assume our job is done.. compare the implementations
#don't rename if immediate predecessor is same code.
#set topstack [lindex $comstacks end]
#set next_impl [dict get $topstack implementation]
set current_body [info body $command]
lassign [commandstack::lib::split_body $current_body] _ current_code
set current_code [string trim $current_code]
set new_code [string trim $procbody]
if {$current_code eq $new_code} {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command with same procbody - Aborting rename."
puts stderr [::commandstack::show_stack $command]
} else {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command - but appears to be with new code - proceeding."
puts stdout "----------"
puts stdout "$current_code"
puts stdout "----------"
puts stdout "$new_code"
puts stdout "----------"
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
}
} else {
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command, but is not immediate predecessor - proceeding anyway... (untested)"
puts stderr
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
}
} elseif {$next_implementor in $::commandstack::known_renamers} {
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
} elseif {$next_implementor in {builtin}} {
#native/builtin could still have been renamed
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
} elseif {$next_implementor in {unspecified undetermined}} {
#could be a standard tcl proc, or from application or package
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
} else {
puts stderr "(commandstack::rename_command) Warning - pkg:'$next_implementor' has renamed the '$command' command. Attempting to cooperate. (untested)"
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
set do_rename 1
}
} else {
#_originalcommand_<mungedcommand>
#assume builtin/original
set next_implementor original
#rename $command $next_target
set do_rename 1
}
#There are of course other ways in which $command may have been renamed - but we can't detect.
set token [list $command $renamer $tokenid]
return [dict create next_target $next_target next_implementor $next_implementor token $token do_rename $do_rename]
} } $command $renamer $procbody]
]
variable debug
if {$debug} {
if {[dict exists $all_stacks $command]} {
set stack [dict get $all_stacks $command]
puts stderr "(commandstack::rename_command) Subsequent rename of command '$command'. (previous renames: [llength $stack]). Renaming to [dict get $nextinfo next_target]"
} else {
#assume this is the original
puts stderr "(commandstack::rename_command) 1st detected rename of command '$command'. Renaming to [dict get $nextinfo next_target]"
}
}
#token is always first dict entry. (Value needs to be searched with lsearch -index 1 )
#renamer is always second dict entry (Value needs to be searched with lsearch -index 3)
set new_record [dict create\
token [dict get $nextinfo token]\
renamer $renamer\
next_implementor [dict get $nextinfo next_implementor]\
next_getter [list ::commandstack::get_next_command {*}[dict get $nextinfo token]]\
implementation [dict get $nextinfo next_target]\
]
if {![dict get $nextinfo do_rename]} {
#review
puts stderr "no rename performed"
return [dict create implementation ""]
}
catch {rename ::commandstack::temp::testproc ""}
set nextinit [string map [list %command% $command %renamer% $renamer %next_getter% [dict get $new_record next_getter] %original_implementation% [dict get $new_record implementation]] {
#IMPLEMENTOR_%renamer%! (mechanism: 'commandstack::rename_command -renamer %renamer% %command% <procargs> <procbody> )
set COMMANDSTACKNEXT_ORIGINAL %original_implementation% ;#informational/debug for overriding proc.
set COMMANDSTACKNEXT [%next_getter%]
#<commandstack_separator>#
}]
set final_procbody "$nextinit$procbody"
#build the proc at a temp location so that if it raises an error we don't adjust the stack or replace the original command
#(e.g due to invalid argument specifiers)
proc ::commandstack::temp::testproc $procargs $final_procbody
uplevel 1 [list rename $command [dict get $nextinfo next_target]]
uplevel 1 [list rename ::commandstack::temp::testproc $command]
dict lappend all_stacks $command $new_record
return $new_record
}
#todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer
#todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost
#todo - removal of all entries pertaining to a particular renamer
#todo - allow restore to bottom-most implementation (original) - regardless of what renamers have cooperated in the stack?
#remove by token, or by commandname if called from same context as original rename_command
#If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed.
#A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything.
#similarly a nonexistant token or renamer will not remove anything and will just return the current stack
proc remove_rename {token_or_command} {
if {[llength $token_or_command] == 3} {
#is token
lassign $token_or_command command renamer tokenid
} elseif {[llength $token_or_command] == 2} {
#command and renamer only supplied
lassign $token_or_command command renamer
set tokenid ""
} elseif {[llength $token_or_command] == 1} {
#is command name only
set command $token_or_command
set renamer [uplevel 1 [list namespace current]]
set tokenid ""
}
set command [uplevel 1 [list namespace which $command]]
variable all_stacks
variable known_renamers
if {$renamer ni $known_renamers} {
error "(commandstack::remove_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers' for command '$command'. Ensure remove_rename called from same context as rename_command was, or explicitly supply exact token or {<command> <renamer>}"
}
if {[dict exists $all_stacks $command]} {
set stack [dict get $all_stacks $command]
if {$tokenid ne ""} {
#token_or_command is a token as returned within the rename_command result dictionary
#search first dict value
set doomed_posn [lsearch -index 1 $stack $token_or_command]
} else {
#search second dict value
set matches [lsearch -all -index 3 $stack $renamer]
set doomed_posn [lindex $matches end] ;#we don't have a full token - pop last entry for this renamer
}
if {$doomed_posn ne "" && $doomed_posn > -1} {
set doomed_record [lindex $stack $doomed_posn]
if {[llength $stack] == ($doomed_posn + 1)} {
#last on stack - put the implemenation from the doomed_record back as the actual command
uplevel #0 [list rename $command ""]
uplevel #0 [list rename [dict get $doomed_record implementation] $command]
} elseif {[llength $stack] > ($doomed_posn + 1)} {
#there is at least one more record on the stack - rewrite it to point where the doomed_record pointed
set rewrite_posn [expr {$doomed_posn + 1}]
set rewrite_record [lindex $stack $rewrite_posn]
if {[dict get $rewrite_record next_implementor] ne $renamer} {
puts stderr "(commandstack::remove_rename) WARNING: next record on the commandstack didn't record '$renamer' as the next_implementor - not deleting implementation [dict get $rewrite_record implementation]"
} else {
uplevel #0 [list rename [dict get $rewrite_record implementation] ""]
}
dict set rewrite_record next_implementor [dict get $doomed_record next_implementor]
#don't update next_getter - it always refers to self
dict set rewrite_record implementation [dict get $doomed_record implementation]
lset stack $rewrite_posn $rewrite_record
dict set all_stacks $command $stack
}
set stack [lreplace $stack $doomed_posn $doomed_posn]
dict set all_stacks $command $stack
}
return $stack
}
return [list]
}
proc show_stack {{commandname_glob *}} {
variable all_stacks
if {![regexp {[?*]} $commandname_glob]} {
#if caller is attempting exact match - use the calling context to resolve in case they didn't supply namespace
set commandname_glob [uplevel 1 [list namespace which $commandname_glob]]
}
if {[package provide punk::lib] ne "" && [package provide punk] ne ""} {
#punk pipeline also needed for patterns
return [punk::lib::pdict -channel none all_stacks $commandname_glob/@*/@*.@*]
} else {
set result ""
set matchedkeys [dict keys $all_stacks $commandname_glob]
#don't try to calculate widest on empty list
if {[llength $matchedkeys]} {
set widest [tcl::mathfunc::max {*}[lmap v $matchedkeys {tcl::string::length $v}]]
set indent [string repeat " " [expr {$widest + 3}]]
set indent2 "${indent} " ;#8 spaces for " i = " where i is 4 wide
set padkey [string repeat " " 20]
foreach k $matchedkeys {
append result "$k = "
set i 0
foreach stackmember [dict get $all_stacks $k] {
if {$i > 0} {
append result "\n$indent"
}
append result [string range "$i " 0 4] " = "
set j 0
dict for {k v} $stackmember {
if {$j > 0} {
append result "\n$indent2"
}
set displaykey [string range "$k$padkey" 0 20]
append result "$displaykey = $v"
incr j
}
incr i
}
append result \n
}
}
return $result
}
}
#review
#document when this is to be called. Wiping stacks without undoing renames seems odd.
proc Delete_stack {command} {
variable all_stacks
if {[dict exists $all_stacks $command]} {
dict unset all_stacks $command
return 1
} else {
return 1
}
}
#can be used to temporarily put a stack aside - should manually rename back when done.
#review - document how/when to use. example? intention?
proc Rename_stack {oldname newname} {
variable all_stacks
if {[dict exists $all_stacks $oldname]} {
if {[dict exists $all_stacks $newname]} {
error "(commandstack::rename_stack) cannot rename $oldname to $newname - $newname already exists in stack"
} else {
#set stackval [dict get $all_stacks $oldname]
#dict unset all_stacks $oldname
#dict set all_stacks $newname $stackval
dict set all_stacks $newname [lindex [list [dict get $all_stacks $oldname] [dict unset all_stacks $oldname]] 0]
}
}
}
}
namespace eval commandstack::lib {
proc splitx {str {regexp {[\t \r\n]+}}} {
#snarfed from tcllib textutil::splitx to avoid the dependency
# Bugfix 476988
if {[string length $str] == 0} {
return {}
}
if {[string length $regexp] == 0} {
return [::split $str ""]
}
if {[regexp $regexp {}]} {
return -code error "splitting on regexp \"$regexp\" would cause infinite loop"
}
set list {}
set start 0
while {[regexp -start $start -indices -- $regexp $str match submatch]} {
foreach {subStart subEnd} $submatch break
foreach {matchStart matchEnd} $match break
incr matchStart -1
incr matchEnd
lappend list [string range $str $start $matchStart]
if {$subStart >= $start} {
lappend list [string range $str $subStart $subEnd]
}
set start $matchEnd
}
lappend list [string range $str $start end]
return $list
}
proc split_body {procbody} {
set marker "#<commandstack_separator>#"
set header ""
set code ""
set found_marker 0
foreach ln [split $procbody \n] {
if {!$found_marker} {
if {[string trim $ln] eq $marker} {
set found_marker 1
} else {
append header $ln \n
}
} else {
append code $ln \n
}
}
if {$found_marker} {
return [list $header $code]
} else {
return [list "" $procbody]
}
}
}
package provide commandstack [namespace eval commandstack {
set version 0.3
}]

201
src/vfs/_vfscommon.vfs/modules/oolib-0.1.2.tm

@ -1,201 +0,0 @@
#JMN - api should be kept in sync with package patternlib where possible
#
package provide oolib [namespace eval oolib {
variable version
set version 0.1.2
}]
namespace eval oolib {
oo::class create collection {
variable o_data ;#dict
#variable o_alias
constructor {} {
set o_data [dict create]
}
method info {} {
return [dict info $o_data]
}
method count {} {
return [dict size $o_data]
}
method isEmpty {} {
expr {[dict size $o_data] == 0}
}
method names {{globOrIdx {}}} {
if {[llength $globOrIdx]} {
if {[string is integer -strict $globOrIdx]} {
set idx $globOrIdx
if {$idx < 0} {
set idx "end-[expr {abs($idx + 1)}]"
}
if {[catch {lindex [dict keys $o_data] $idx} result]} {
error "[self object] no such index : '$idx'"
} else {
return $result
}
} else {
#glob
return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx]
}
} else {
return [dict keys $o_data]
}
}
#like names but without globbing
method keys {} {
dict keys $o_data
}
method key {{posn 0}} {
if {$posn < 0} {
set posn "end-[expr {abs($posn + 1)}]"
}
if {[catch {lindex [dict keys $o_data] $posn} result]} {
error "[self object] no such index : '$posn'"
} else {
return $result
}
}
method hasKey {key} {
dict exists $o_data $key
}
method get {} {
return $o_data
}
method items {} {
return [dict values $o_data]
}
method item {key} {
if {[string is integer -strict $key]} {
if {$key >= 0} {
set valposn [expr {(2*$key) +1}]
return [lindex $o_data $valposn]
} else {
set key "end-[expr {abs($key + 1)}]"
return [lindex $o_data $key]
#return [lindex [dict keys $o_data] $key]
}
}
if {[dict exists $o_data $key]} {
return [dict get $o_data $key]
}
}
#inverse lookup
method itemKeys {value} {
set value_indices [lsearch -all [dict values $o_data] $value]
set keylist [list]
foreach i $value_indices {
set idx [expr {(($i + 1) *2) -2}]
lappend keylist [lindex $o_data $idx]
}
return $keylist
}
method search {value args} {
set matches [lsearch {*}$args [dict values $o_data] $value]
if {"-inline" in $args} {
return $matches
} else {
set keylist [list]
foreach i $matches {
set idx [expr {(($i + 1) *2) -2}]
lappend keylist [lindex $o_data $idx]
}
return $keylist
}
}
#review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists?
#review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well?
#method alias {newAlias existingKeyOrAlias} {
# if {[string is integer -strict $newAlias]} {
# error "[self object] collection key alias cannot be integer"
# }
# if {[string length $existingKeyOrAlias]} {
# set o_alias($newAlias) $existingKeyOrAlias
# } else {
# unset o_alias($newAlias)
# }
#}
#method aliases {{key ""}} {
# if {[string length $key]} {
# set result [list]
# foreach {n v} [array get o_alias] {
# if {$v eq $key} {
# lappend result $n $v
# }
# }
# return $result
# } else {
# return [array get o_alias]
# }
#}
##if the supplied index is an alias, return the underlying key; else return the index supplied.
#method realKey {idx} {
# if {[catch {set o_alias($idx)} key]} {
# return $idx
# } else {
# return $key
# }
#}
method add {value key} {
if {[string is integer -strict $key]} {
error "[self object] collection key must not be an integer. Use another structure if integer keys required"
}
if {[dict exists $o_data $key]} {
error "[self object] col_processors object error: key '$key' already exists in collection"
}
dict set o_data $key $value
return [expr {[dict size $o_data] - 1}] ;#return index of item
}
method remove {idx {endRange ""}} {
if {[string length $endRange]} {
error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time"
}
if {[string is integer -strict $idx]} {
if {$idx < 0} {
set idx "end-[expr {abs($idx+1)}]"
}
set key [lindex [dict keys $o_data] $idx]
set posn $idx
} else {
set key $idx
set posn [lsearch -exact [dict keys $o_data] $key]
if {$posn < 0} {
error "[self object] no such index: '$idx' in this collection"
}
}
dict unset o_data $key
return
}
method clear {} {
set o_data [dict create]
return
}
method reverse_the_collection {} {
#named slightly obtusely because reversing the data when there may be references held is a potential source of bugs
#the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy.
#todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations.
set dictnew [dict create]
foreach k [lreverse [dict keys $o_data]] {
dict set dictnew $k [dict get $o_data $k]
}
set o_data $dictnew
return
}
#review - cmd as list vs cmd as script?
method map {cmd} {
set seed [list]
dict for {k v} $o_data {
lappend seed [uplevel #0 [list {*}$cmd $v]]
}
return $seed
}
method objectmap {cmd} {
set seed [list]
dict for {k v} $o_data {
lappend seed [uplevel #0 [list $v {*}$cmd]]
}
return $seed
}
}
}

7
src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.1.tm

@ -1325,6 +1325,13 @@ tcl::namespace::eval punk::ansi {
grepstr -r m -C 2 {cancelid} [s ::after] ;#with 2 lines of context and no break indicators.
}]}
To match a literal string that contains regex special characters, it is in some cases easier to use the Tcl regex
quoting switch (?q) to quote the string rather than escaping each special character with a backslash.
${[punk::args::helpers::example -syntax none {
#The following example matches a literal string that is the Tcl expansion operator {*}
grepstr "(?q){*}" $sometext
}]}
}
@leaders -min 0 -max 0

14
src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.3.1.tm

@ -474,7 +474,7 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
-max_depth 1000
-max-depth 1000
-subdirlist {}
-punkcheck_eventobj "\uFFFF"
-glob *.tm
@ -487,7 +487,7 @@ namespace eval punk::mix::cli {
set installername [dict get $opts -installer]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set CALLDEPTH [dict get $opts -call-depth-internal]
set max_depth [dict get $opts -max_depth]
set max_depth [dict get $opts -max-depth]
set subdirlist [dict get $opts -subdirlist]
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set fileglob [dict get $opts -glob]
@ -533,7 +533,7 @@ namespace eval punk::mix::cli {
set config [dict create {*}{
-glob $fileglob
-max_depth 0
-max-depth 0
}]
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
@ -628,11 +628,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
@ -829,11 +829,11 @@ namespace eval punk::mix::cli {
# -- ---
set config [dict create {*}{
-glob *
-max_depth 100
-max-depth 100
}]
set had_error 0
# -max_depth -1 for no limit
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder

1427
src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.4.0.tm

File diff suppressed because it is too large Load Diff

324
src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/doc-0.1.1.tm

@ -0,0 +1,324 @@
# -*- 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 punk::mix::commandset::doc 0.1.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require punk::path ;# for treefilenames, relative
package require punk::repo
package require punk::docgen ;#inline doctools - generate doctools .man files at src/docgen prior to using kettle to producing .html .md etc
package require punk::mix::cli ;#punk::mix::cli::lib used for kettle_call
#package require punkcheck ;#for path_relative
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::mix::commandset::doc {
namespace export *
proc _default {} {
puts "documentation subsystem"
puts "commands: doc.build"
puts " build documentation from src/doc to src/embedded using the kettle build tool"
puts "commands: doc.status"
}
proc build {} {
puts "build docs"
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to build docs"
return
}
#user may delete the comment containing "--- punk::docgen::overwrites" and then manually edit, and we won't overwrite
#we still generate output in src/docgen so user can diff and manually update if thats what they prefer
set oldfiles [punk::path::treefilenames -dir $projectdir/src/doc _module_*.man]
foreach maybedoomed $oldfiles {
set fd [open $maybedoomed r]
chan conf $fd -translation binary
set data [read $fd]
close $fd
if {[string match "*--- punk::docgen overwrites *" $data]} {
file delete -force $maybedoomed
}
}
set generated [lib::do_docgen modules]
if {[dict get $generated count] > 0} {
#review
set doclist [dict get $generated docs]
set source_base [dict get $generated base]
set target_base $projectdir/src/doc
foreach dinfo $doclist {
lassign $dinfo module fpath
set relpath [punk::path::relative $source_base $fpath]
set relfolder [file dirname $relpath]
if {$relfolder eq "."} {
set relfolder ""
}
file mkdir [file join $target_base $relfolder]
set target [file join $target_base $relfolder _module_[file tail $fpath]]
puts stderr "target --> $target"
if {![file exists $target]} {
file copy $fpath $target
}
}
}
if {[file exists $projectdir/src/doc]} {
set original_wd [pwd]
cd $projectdir/src
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init VIRTUAL kettle_build_doc ;#VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
if {\
[llength [dict get [$event targetset_source_changes] changed]]\
} {
$event targetset_started
# -- --- --- --- --- ---
puts stdout "BUILDING DOCS at $projectdir/src/embedded from src/doc"
if {[catch {
if {"::meta" eq [info commands ::meta]} {
puts stderr "There appears to be a leftover ::meta command which is presumed to be from doctools. Destroying object"
::meta destroy
}
punk::mix::cli::lib::kettle_call lib doc
#Kettle doc
} errM]} {
$event targetset_end FAILED -note "kettle_build_doc failed: $errM"
} else {
$event targetset_end OK
}
# -- --- --- --- --- ---
} else {
puts stderr "No change detected in src/doc"
$event targetset_end SKIPPED
}
$event end
$event destroy
$installer destroy
cd $original_wd
} else {
puts stderr "No doc folder found at $projectdir/src/doc"
}
}
proc status {} {
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
cd $projectdir/src
puts stdout "Testing status of doctools source location $projectdir/src/doc ..."
flush stdout
#----------
set installer [punkcheck::installtrack new project.new $projectdir/src/.punkcheck]
$installer set_source_target $projectdir/src/doc $projectdir/src/embedded
set event [$installer start_event {-install_step kettledoc}]
#use same virtual id "kettle_build_doc" as project.new - review best way to keep identifiers like this in sync.
$event targetset_init QUERY kettle_build_doc ;#usually VIRTUAL - since there is no specific target file - and we don't know all the files that will be generated - but here we use QUERY to ensure no writes to .punkcheck
set last_completion [$event targetset_last_complete]
if {[llength $last_completion]} {
#adding a source causes it to be checksummed
$event targetset_addsource $projectdir/src/doc ;#whole doc tree is considered the source
#----------
set changeinfo [$event targetset_source_changes]
if {\
[llength [dict get $changeinfo changed]]\
} {
puts stdout "changed"
puts stdout $changeinfo
} else {
puts stdout "No changes detected in $projectdir/src/doc tree"
}
} else {
#no previous completion-record for this target - must assume changed - no need to trigger checksumming
puts stdout "No existing record of doc build in .punkcheck. Assume it needs to be rebuilt."
}
$event destroy
$installer destroy
cd $original_wd
}
proc validate {args} {
set argd [punk::args::parse $args withdef {
@id -id ::punk::mix::commandset::doc::validate
-- -type none -optional 1 -help "end of options marker --"
-individual -type boolean -default 1
@values -min 0 -max -1
patterns -default {*.man} -type any -multiple 1
}]
set opt_individual [tcl::dict::get $argd opts -individual]
set patterns [tcl::dict::get $argd values patterns]
#todo - run and validate punk::docgen output
set projectdir [punk::repo::find_project]
if {$projectdir eq ""} {
puts stderr "No current project dir - unable to check doc status"
return
}
if {![file exists $projectdir/src/doc]} {
set result "No documentation source found. Expected .man files in doctools format at $projectdir/src/doc"
return $result
}
set original_wd [pwd]
set docroot $projectdir/src/doc
cd $docroot
if {!$opt_individual && "*.man" in $patterns} {
if {[catch {
dtplite validate $docroot
} errM]} {
puts stderr "commandset::doc::validate failed for projectdir '$projectdir'"
puts stderr "docroot '$docroot'"
puts stderr "dtplite error was: $errM"
}
} else {
foreach p $patterns {
set treefiles [punk::path::treefilenames $p]
foreach path $treefiles {
puts stdout "dtplite validate $path"
dtplite validate $path
}
}
}
#punk::mix::cli::lib::kettle_call lib validate-doc
cd $original_wd
}
namespace eval collection {
variable pkg
set pkg punk::mix::commandset::doc
namespace export *
namespace path [namespace parent]
}
namespace eval lib {
variable pkg
set pkg punk::mix::commandset::doc
proc do_docgen {{project_subpath modules}} {
#Extract doctools comments from source code
set projectdir [punk::repo::find_project]
set output_base [file join $projectdir src docgen]
set codesource_path [file join $projectdir $project_subpath]
if {![file isdirectory $codesource_path]} {
puts stderr "WARNING punk::mix::commandset::doc unable to find codesource_path $codesource_path during do_docgen - skipping inline doctools generation"
return
}
if {[file isdirectory $output_base]} {
if {[catch {
file delete -force $output_base
}]} {
error "do_docgen failed to delete existing output base folder: $output_base"
}
}
file mkdir $output_base
set matched_paths [punk::path::treefilenames -dir $codesource_path -exclude-paths {**/mix/templates/** **/project_layouts/** **/decktemplates/** **/_aside **/_aside/**} *.tm]
set count 0
set newdocs [list]
set docgen_header_comments ""
append docgen_header_comments {[comment {--- punk::docgen generated from inline doctools comments ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen DO NOT EDIT DOCS HERE UNLESS YOU REMOVE THESE COMMENT LINES ---}]} \n
append docgen_header_comments {[comment {--- punk::docgen overwrites this file ---}]} \n
foreach fullpath $matched_paths {
puts stdout "do_docgen processing: $fullpath"
set doctools [punk::docgen::get_doctools_comments $fullpath]
if {$doctools ne ""} {
set fname [file tail $fullpath]
set mod_tail [file rootname $fname]
set relpath [punk::path::relative $codesource_path [file dirname $fullpath]]
if {$relpath eq "."} {
set relpath ""
}
set tailsegs [file split $relpath]
set module_fullname [join $tailsegs ::]::$mod_tail
set target_docname $fname.man
set this_outdir [file join $output_base $relpath]
if {[string length $fname] > 99} {
#output needs to be tarballed to do checksum change tests in a reasonably straightforward and not-too-terribly slow way.
#hack - review. Determine exact limit - test if tcllib tar fixed or if it's a limit of the particular tar format
#work around tcllib tar filename length limit ( somewhere around 100?) This seems to be a limit on the length of a particular segment in the path.. not whole path length?
#this case only came up because docgen used to path munge to long filenames - but left because we know there is a limit and renaming fixes it - even if it's ugly - but still allows doc generation.
#review - if we're checking fname - should also test length of whole path and determine limits for tar
package require md5
if {[package vsatisfies [package present md5] 2- ] } {
set md5opt "-hex"
} else {
set md5opt ""
}
set target_docname [md5::md5 {*}$md5opt [encoding convertto utf-8 $fullpath]]_overlongfilename.man
puts stderr "WARNING - overlong file name - renaming $fullpath"
puts stderr " to [file dirname $fullpath]/$target_docname"
}
file mkdir $this_outdir
puts stdout "saving [string length $doctools] bytes of doctools output from file $relpath/$fname"
set outfile [file join $this_outdir $target_docname]
set fd [open $outfile w]
fconfigure $fd -translation binary
puts -nonewline $fd $docgen_header_comments$doctools
close $fd
incr count
lappend newdocs [list $module_fullname $outfile]
}
}
return [list count $count docs $newdocs base $output_base]
}
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::mix::commandset::doc [namespace eval punk::mix::commandset::doc {
variable pkg punk::mix::commandset::doc
variable version
set version 0.1.1
}]
return

1178
src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.1.tm

File diff suppressed because it is too large Load Diff

1178
src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.2.0.tm

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

70
src/vfs/_vfscommon.vfs/modules/punk/path-0.1.0.tm

@ -1444,52 +1444,34 @@ namespace eval punk::path {
return [treefile_priv::walk_treefilenames_zipfs $state]
}
punk::args::define {
@id -id ::punk::path::relative
@cmd -name punk::path::relative\
-summary\
"Compute the relative path from a reference path to a location path."\
-help\
"Taking two directory paths, a reference and a location, computes the path
of the location relative to the reference.
Will return a single dot '.' if the paths are the same.
Both paths must be the same type - ie both absolute or both relative.
Matching is case sensitive. On windows, the drive-letter component (only) is
not case sensitive, so punk::path::relative c:/etc C:/etc returns '.'.
The part following the driveletter is case sensitive, so
punk::path::relative c:/etc C:/Etc returns ../Etc.
On windows, if the paths are absolute and specify different volumes,
only the location will be returned."
@leaders -min 2 -max 2
reference -type string -help\
"The path from which the relative path to location is determined."
location -type string -help\
"The location path which may be above or below the reference path."
}
#maint warning - also in punkcheck
proc relative {reference location} {
#*** !doctools
#[call [fun relative] [arg reference] [arg location]]
#[para] Taking two directory paths, a reference and a location, computes the path
# of the location relative to the reference.
#[list_begin itemized]
#[item]
#[para] Arguments:
# [list_begin arguments]
# [arg_def string reference] The path from which the relative path to location is determined.
# [arg_def string location] The location path which may be above or below the reference path
# [list_end]
#[item]
#[para] Results:
#[para] The relative path of the location to the reference path.
#[para] Will return a single dot "." if the paths are the same
#[item]
#[para] Notes:
#[para] Both paths must be the same type - ie both absolute or both relative
#[para] Case sensitive. ie punk::path::relative /etc /etC
# will return ../etC
#[para] On windows, the drive-letter component (only) is not case sensitive
#[example_begin]
# P% punk::path::relative c:/etc C:/etc
# - .
#[example_end]
#[para] The part following the driveletter is case sensitive so in the following cases it recognises the driveletter matches but not the tail
#[example_begin]
# P% punk::path::relative c:/etc C:/Etc
# - ../Etc
#[example_end]
#[para] On windows, if the paths are absolute and specifiy different volumes, only the location will be returned.
#[example_begin]
# P% punk::path::relative c:/etc d:/etc/blah
# - d:/etc/blah
#[example_end]
#[para] Unix-like examples:
#[example_begin]
# P% punk::path::relative /usr/local/etc/ /usr/local/etc/somewhere/below
# - somewhere/below
# P% punk::path::relative /usr/local/etc/somewhere /usr/local/lib/here
# - ../../lib/here
#[example_end]
#[list_end]
#see also kettle
# Modified copy of ::fileutil::relative (tcllib)
# Adapted to 8.5 ({*}).

1611
src/vfs/_vfscommon.vfs/modules/punk/path-0.2.0.tm

File diff suppressed because it is too large Load Diff

2479
src/vfs/_vfscommon.vfs/modules/punkcheck-0.2.0.tm

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save