From c5dc332e187597960a1dc6739632d7976221a6b2 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Mon, 28 Jul 2025 03:21:31 +1000 Subject: [PATCH] make.tcl fixes and scriptwrap fixes, sdx.kit --- src/bin/sdx.kit | Bin 0 -> 119312 bytes .../modules/punk/libunknown-0.1.tm | 8 +- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 455 +++++++---- src/bootsupport/modules/punk/ns-0.1.0.tm | 226 ++++-- src/bootsupport/modules/punk/repl-0.1.2.tm | 2 +- src/bootsupport/modules/textblock-0.1.3.tm | 2 +- src/lib/app-shellspy/shellspy.tcl | 43 +- src/make.tcl | 108 ++- src/modules/punk/libunknown-0.1.tm | 8 +- .../mix/commandset/scriptwrap-999999.0a1.0.tm | 455 +++++++---- .../utility/scriptappwrappers/multishell.cmd | 29 +- src/modules/punk/ns-999999.0a1.0.tm | 226 ++++-- src/modules/punk/repl-999999.0a1.0.tm | 2 +- src/modules/textblock-999999.0a1.0.tm | 2 +- .../custom/_project/punk.basic/src/make.tcl | 108 ++- .../modules/punk/libunknown-0.1.tm | 8 +- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 455 +++++++---- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 226 ++++-- .../bootsupport/modules/punk/repl-0.1.2.tm | 2 +- .../bootsupport/modules/textblock-0.1.3.tm | 2 +- .../_project/punk.project-0.1/src/make.tcl | 108 ++- .../modules/punk/libunknown-0.1.tm | 8 +- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 455 +++++++---- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 226 ++++-- .../bootsupport/modules/punk/repl-0.1.2.tm | 2 +- .../bootsupport/modules/textblock-0.1.3.tm | 2 +- .../_project/punk.shell-0.1/src/make.tcl | 108 ++- src/runtime/mapvfs.config | 2 +- src/scriptapps/example.sh | 1 + src/scriptapps/example.tcl | 1 + src/scriptapps/example_out.bat | 743 ++++++++++++++++++ src/scriptapps/example_wrap.toml | 41 + src/vfs/_config/punk_main.tcl | 18 +- .../lib/app-shellspy/shellspy.tcl | 43 +- .../modules/punk/libunknown-0.1.tm | 8 +- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 455 +++++++---- .../utility/scriptappwrappers/multishell.cmd | 29 +- .../_vfscommon.vfs/modules/punk/ns-0.1.0.tm | 226 ++++-- .../_vfscommon.vfs/modules/punk/repl-0.1.2.tm | 2 +- .../_vfscommon.vfs/modules/textblock-0.1.3.tm | 2 +- 40 files changed, 3643 insertions(+), 1204 deletions(-) create mode 100644 src/bin/sdx.kit create mode 100644 src/scriptapps/example.sh create mode 100644 src/scriptapps/example.tcl create mode 100644 src/scriptapps/example_out.bat create mode 100644 src/scriptapps/example_wrap.toml diff --git a/src/bin/sdx.kit b/src/bin/sdx.kit new file mode 100644 index 0000000000000000000000000000000000000000..4c70d7e76c42527e90dc43e5c1d05b5942a00eff GIT binary patch literal 119312 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�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 literal 0 HcmV?d00001 diff --git a/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/bootsupport/modules/punk/libunknown-0.1.tm index a4f56010..1b15d45a 100644 --- a/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -890,10 +890,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - puts stderr "--> pkg epoch $prev_e -> $current_e" - puts stderr "args: $args" - puts stderr "last_auto: $last_auto_path" - puts stderr "auto_path: $auto_path" + #puts stderr "--> pkg epoch $prev_e -> $current_e" + #puts stderr "args: $args" + #puts stderr "last_auto: $last_auto_path" + #puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries diff --git a/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 8ef36e27..06b145de 100644 --- a/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -20,7 +20,7 @@ #[manpage_begin punkshell_module_scriptwrap 0 0.1.0] #[copyright "2024"] #[titledesc {scriptwrap polyglot tool}] [comment {-- Name section and table of contents description --}] -#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] +#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] #[require punk::mix::commandset::scriptwrap] #[keywords module commandset launcher scriptwrap] #[description] @@ -30,7 +30,7 @@ #*** !doctools #[section Overview] -#[para] overview of scriptwrap +#[para] overview of scriptwrap #[subsection Concepts] #[para] - @@ -74,7 +74,7 @@ package require punk::fileline namespace eval punk::mix::commandset::scriptwrap { #*** !doctools #[subsection {Namespace punk::mix::commandset::scriptwrap}] - #[para] Core API functions for punk::mix::commandset::scriptwrap + #[para] Core API functions for punk::mix::commandset::scriptwrap #[list_begin definitions] namespace export * @@ -93,7 +93,7 @@ namespace eval punk::mix::commandset::scriptwrap { foreach k [lreverse [dict keys $tdict_low_to_high]] { dict set tdict $k [dict get $tdict_low_to_high $k] } - + #set pathinfolist [dict values $tdict] set names [dict keys $tdict] @@ -142,9 +142,9 @@ namespace eval punk::mix::commandset::scriptwrap { put stderr "commandset::scriptwrap::templates_dict WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } return - } - - + } + + #A batch file with unix line-endings is sensitive to label positioning. #batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it. #see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings) @@ -808,176 +808,317 @@ namespace eval punk::mix::commandset::scriptwrap { return $result } #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site - #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf - proc multishell {filepath_or_scriptset args} { - set opts [dict create\ - -askme 1\ - -outputfolder "\uFFFF"\ - -template "\uFFFF"\ - -returnextra 0\ - -force 0\ - ] - #set known_opts [dict keys $defaults] - foreach {k v} $args { - switch -- $k { - -askme - -outputfolder - -template - -returnextra - -force { - dict set opts $k $v - } - default { - error "punk::mix::commandset::multishell error. Unrecognized option '$k'. Known-options: [dict keys $opts]" - } + #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf + #set usage "" + #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n + #append usage "The scriptset name will be used to search for .sh|.tcl|.ps1 or names as you specify in yourname.wrapconfig if it exists" \n + #append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n + #if {![string length $filepath_or_scriptset]} { + # puts stderr "No filepath_or_scriptset specified" + # puts stderr $usage + # return false + #} + proc _read_scriptset_wrap_tomlfile {fname} { + set resultd [dict create] + package require tomlish + set tomldata [readFile $fname] + #todo - fix tomlish to provide line number in ERROR structure during from_toml call. + if {[catch {tomlish::to_dict [tomlish::from_toml $tomldata]} tomldict]} { + puts stderr "Failed to parse $fname" + puts stderr "error: $tomldict" + } + if {[tomlish::dict::path::exists $tomldict {.application.template}]} { + dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] + } + set scripts [list] + if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { + set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] + foreach tvdict $arrvalues { + lappend scripts [dict get $tvdict value] } } + dict set resultd scripts $scripts - set usage "" - append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n - append usage "The scriptset name will be used to search for yourname.sh|tcl|ps1 or names as you specify in yourname.wrapconfig if it exists" \n - append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n - if {![string length $filepath_or_scriptset]} { - puts stderr "No filepath_or_scriptset specified" - puts stderr $usage - return false + set ftail [file rootname [file tail $fname]] ;#e.g example_wrap.toml + set scriptset [lindex [split $ftail _] 0] + set fallback_outputfile $scriptset.cmd + set fallback_nextshellpath "/usr/bin/env tclsh" + set fallback_nextshelltype "tcl" + + if {[tomlish::dict::path::exists $tomldict {.application.default_outputfile.value}]} { + dict set resultd default_outputfile [tomlish::dict::path::get $tomldict {.application.default_outputfile.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshellpath.value}]} { + dict set resultd default_nextshellpath [tomlish::dict::path::get $tomldict {.application.default_nextshellpath.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshelltype.value}]} { + dict set resultd default_nextshelltype [tomlish::dict::path::get $tomldict {.application.default_nextshelltype.value}] + } + foreach platform {win32 dragonflybsd freebsd netbsd linux macosx other} { + set d [dict create] + foreach field {outputfile nextshellpath nextshelltype} { + if {[tomlish::dict::path::exists $tomldict ".application.$platform.$field.value"]} { + dict set d $field [tomlish::dict::path::get $tomldict ".application.$platform.$field.value"] + } else { + if {[dict exists $resultd default_$field]} { + dict set d $field [dict get $resultd default_$field] + } else { + dict set d $field [set fallback_$field] + } + } + } + dict set resultd $platform $d } + + return $resultd + } + punk::args::define { + @id -id ::punk::mix::commandset::scriptwrap::multishell + @cmd -name punk::mix::commandset::scriptwrap::multishell\ + -summary\ + "Wrap script(s) into a polyglot cross-platform executable script."\ + -help\ + "Create a polyglot executable script that wraps constituent scripts written in + various scripting languages such as perl, tcl, shell script, powershell. + The resulting polyglot file should run cross platform on windows and various + types of unix-like OS. For use on windows the output file should be named with + a .bat or .cmd extension - but the same file with extension removed should also + be capable of running on FreeBSD, Linux etc. + Note that a polyglot script such as this may be somewhat brittle over the long + term with regards to default shells and scripting languages across platforms." + @leaders -min 1 -max 1 + filepath_or_scriptset -type string -minsize 1 -help\ + "Supply the path to a single script file to wrap, or the name of a scriptset. + The scriptset name will be used to search for .sh|.bash|.tcl|.ps1|.pl + or alternatively, names as specified in a configuration file named _wrap.toml + if it exists in the current folder, or is specified with a full path name. + If no template name/path is specified in a _wrap.toml file and no + -template argument is supplied the default punk.multishell.cmd will be used. + If the template is specified explicitly in -template as well as in the .toml + file - the supplied -template argument will override that specified in the + .toml file." + @opts + -template -type string -default "punk.multishell.cmd" -help\ + "Templates are provided from modules or paths in the current project, + so available templates will vary based on whether the multishell + command is being run from within a project directory or not. + To see available templates use punk::mix::commandset::scriptwrap::templates." + -outputfolder -type directory -default "" -help\ + "Folder to which to write resulting polyglot script. + If empty, the output will go to the /bin folder or + to the current working directory if there is no projectroot." + -askme -type boolean -default 1 -help\ + "Prompt user at console (stdin) for confirmation of operations such as + overwrite." + -force -type boolean -default 0 + -returnextra -type boolean -default 0 + @values -minvalues 0 -maxvalues 0 + } + #: + #@SET "nextshellpath[win32___________]=tclsh___________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[other___________]=tcl_____________" + #: + proc multishell {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] + lassign [dict values $argd] leaders opts values received + # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_askme [dict get $opts -askme] - set opt_template [dict get $opts -template] - set opt_outputfolder [dict get $opts -outputfolder] - set opt_returnextra [dict get $opts -returnextra] - set opt_force [dict get $opts -force] + set filepath_or_scriptset [dict get $leaders filepath_or_scriptset] + set opt_askme [dict get $opts -askme] + set opt_template [dict get $opts -template] ;#use dict exists $received -template to see if overridable in .toml + set opt_outputfolder [dict get $opts -outputfolder] + set opt_returnextra [dict get $opts -returnextra] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- - set ext [file extension $filepath_or_scriptset] set startdir [pwd] + set allowed_extensions [list tcl ps1 sh bash pl] + #TODO - distinct sections for sh vs bash? needs experiments.. + #for now we use shell-pre-launch-subprocess etc + #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] + set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] + + if {[file pathtype $filepath_or_scriptset] ni {absolute relative}} { + error "bad pathtype for '$filepath_or_scriptset' (expected absolute or relative path, or name of scriptset)" + } - - - #first check if relative or absolute path matches a file + #first check if absolute path matches a file or relative path from cwd matches a file if {[file pathtype $filepath_or_scriptset] eq "absolute"} { - set specified_path $filepath_or_scriptset + set specified_path $filepath_or_scriptset } else { set specified_path [file join $startdir $filepath_or_scriptset] } + set scriptdir [file dirname $specified_path] + set ext [string trim [file extension $filepath_or_scriptset] .] - set allowed_extensions [list wrapconfig tcl ps1 sh bash pl] - set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] - #set allowed_extensions [list tcl] - set found_script 0 - if {[file exists $specified_path]} { - set found_script 1 + set scriptset "" + if {$ext eq ""} { + set scriptset [file rootname [file tail $specified_path]] + } elseif {$ext eq "toml"} { + set tomltail [file tail $specified_path] + if {[string match *_wrap.toml $tomltail]} { + set scriptset [lindex [split $tomltail _] 0] + #if .toml was specified - the config file must exist + if {![file exists $specified_path]} { + if {[file pathtype $filepath_or_scriptset] eq "relative"} { + puts stderr "unable to locate '$specified_path' - will continue search in src/scriptapps folder" + } else { + #caller was specific about path - no fallback to src/scriptapps + error "unable to locate '$specified_path'" + } + } + } else { + error "supplied toml file must be of form _wrap.toml" + } } else { - foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { - if {[file exists $filepath_or_scriptset.$e]} { - set found_script 1 - break + if {$ext ni $allowed_extensions} { + error "supplied filepath_or_scriptset must be the name of a scriptset without extension, a file named _wrap.toml, or a script with one of the extensions: $allowed_extensions" + } + } + + set list_input_files [list] + set configd [dict create] + if {$scriptset ne ""} { + puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" + #.toml file may or may not exist + if {[file exists ${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptdir $s] + } + } + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" + return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptdir" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptdir/$scriptset.$e]} { + lappend list_input_files $scriptdir/$scriptset.$e + } } } + } else { + #expect a single script + if {[file exists $specified_path]} { + lappend list_input_files $specified_path + } } + set found_script [expr {[llength $list_input_files] > 0}] #TODO! - use get_wrapper_folders - multishell should use same available templates as the 'templates' function - set scriptset [file rootname [file tail $specified_path]] if {$found_script} { - if {[file type $specified_path] eq "file"} { - set specified_root [file dirname $specified_path] - set pathinfo [punk::repo::find_repos [file dirname $specified_path]] - set projectroot [dict get $pathinfo closest] + #found scripts at absolute path - or path relative to cwd + set scriptroot $scriptdir + set pathinfo [punk::repo::find_repos $scriptroot] + set projectroot [dict get $pathinfo closest] + if {[file exists $scriptroot/wrappers]} { + set customwrapper_folder $scriptroot/wrappers + } else { + #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder if {[string length $projectroot]} { - #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - set customwrapper_folder $projectroot/src/scriptapps/wrappers - } + set customwrapper_folder $projectroot/src/scriptapps/wrappers } else { #outside of any project - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - #no customwrapper folder available - set customwrapper_folder "" - } + set customwrapper_folder "" } - } else { - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false } } else { + if {[file pathtype $filepath_or_scriptset] eq "absolute"} { + return false + } set pathinfo [punk::repo::find_repos $startdir] set projectroot [dict get $pathinfo closest] - if {[string length $projectroot]} { - if {[llength [file split $filepath_or_scriptset]] > 1} { - puts stderr "filepath_or_scriptset looks like a path - but doesn't seem to point to a file" - puts stderr "Ensure you are within a project and use just the name of the scriptset, or pass in the full correct path or relative path to current directory" - puts stderr $usage - return false - } else { - #we've already ruled out empty string - so must have a single element representing scriptset - possibly with file extension - set scriptroot $projectroot/src/scriptapps - set customwrapper_folder $projectroot/src/scriptapps/wrappers - #check something matches the scriptset.. - set something_found "" - if {[file exists $scriptroot/$scriptset]} { - set found_script 1 - set something_found $scriptroot/$scriptset ;#extensionless file - that's ok too - } else { - foreach e $allowed_extensions { - if {[file exists $scriptroot/$scriptset.$e]} { - set found_script 1 - set something_found $scriptroot/$scriptset.$e - break - } + if {![string length $projectroot]} { + puts stderr "No matching scripts or config found for $filepath_or_scriptset, and you are not within a directory where projectroot and src/scriptapps can be determined" + return false + } + + set scriptroot $projectroot/src/scriptapps + set customwrapper_folder $projectroot/src/scriptapps/wrappers + #check something matches the scriptset.. + if {$scriptset ne ""} { + #.toml file may or may not exist + if {[file exists $scriptroot/${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptroot $s] } } - if {!$found_script} { - puts stderr "Searched within $scriptroot" - puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" - puts stderr $usage + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" return false - } else { - if {[file type $something_found] ne "file"} { - puts stderr "Found '$something_found'" - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptroot" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptroot/$scriptset.$e]} { + lappend list_input_files $scriptroot/$scriptset.$e } } } - } else { - puts stderr "filepath_or_scriptset parameter doesn't seem to refer to a file, and you are not within a directory where projectroot and src/scriptapps/wrappers can be determined" - puts stderr $usage - return false + #expect a single script + if {[file exists $scriptroot/$filepath_or_scriptset]} { + if {[file type $scriptroot/$filepath_or_scriptset] ne "file"} { + puts stderr "wrap_in_multishell doesn't currently support a directory as the path. path: $scriptroot/$filepath_or_scriptset" + return false + } + lappend list_input_files $scriptroot/$filepath_or_scriptset + } } - } - #assertion - customwrapper_folder var exists - but might be empty + set found_script [expr {[llength $list_input_files] > 0}] - - if {[string length $ext]} { - #If there was an explicitly supplied extension - then that file should exist - if {![file exists $scriptroot/$scriptset.$ext]} { - puts stderr "Explicit extension .$ext was supplied - but matching file not found." - puts stderr $usage - return false - } else { - if {$ext eq "wrapconfig"} { - set process_extensions ALLFOUNDORCONFIGURED + #---------------------- + if {!$found_script} { + puts stderr "Searched within $scriptdir and $scriptroot" + if {$scriptset ne ""} { + puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" } else { - set process_extensions $ext + puts stderr "Unable to find file $filepath_or_scriptset" } + return false } - } else { - #no explicit extension - process all for scriptset - set process_extensions ALLFOUNDORCONFIGURED + } - #process_extensions - either a single one - or all found or as per .wrapconfig + #assertion - customwrapper_folder var exists - but might be empty - if {$opt_template eq "\uFFFF"} { - set templatename punk.multishell.cmd + if {[dict exists $configd template]} { + set templatename [dict get $configd template] } else { - set templatename $opt_template + if {$opt_template eq "\uFFFF"} { + set templatename punk.multishell.cmd + } else { + set templatename $opt_template + } } set templatename_root [file rootname [file tail $templatename]] @@ -995,7 +1136,7 @@ namespace eval punk::mix::commandset::scriptwrap { set template_base_dict [punk::mix::base::lib::get_template_basefolders] set tpldirs [list] dict for {tdir tsourceinfo} $template_base_dict { - set vendor [dict get $tsourceinfo vendor] + set vendor [dict get $tsourceinfo vendor] if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { lappend tpldirs $tdir } elseif {[file exists $tdir/utility/scriptappwrappers/${templatename_fileroot}[file extension $templatename]]} { @@ -1032,7 +1173,7 @@ namespace eval punk::mix::commandset::scriptwrap { } - if {$opt_outputfolder eq "\uFFFF"} { + if {$opt_outputfolder eq ""} { #outputfolder not explicitly specified by caller if {[string length $projectroot]} { set output_folder [file join $projectroot/bin] @@ -1056,13 +1197,36 @@ namespace eval punk::mix::commandset::scriptwrap { #todo - #output_file extension may also depend on the template being used.. and/or the .wrapconfig - if {$::tcl_platform(platform) eq "windows"} { - set output_extension cmd + #output_file extension may also depend on the template being used.. and/or the _wrap.toml config + + if {[dict size $configd]} { + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other + } + } + set out [dict get $configd $ptype outputfile] + set output_file [file join $output_folder $out] } else { - set output_extension sh + #no _wrap.toml file available + if {$::tcl_platform(platform) eq "windows"} { + set output_extension .cmd + } else { + set output_extension .sh + } + if {$scriptset ne ""} { + set output_file [file join $output_folder $scriptset$output_extension] + } else { + set infile [lindex $list_input_files 0] + set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] + } } - set output_file [file join $output_folder $scriptset.$output_extension] + + if {[file exists $output_file]} { set fdexisting [open $output_file r] fconfigure $fdexisting -translation binary @@ -1103,13 +1267,10 @@ namespace eval punk::mix::commandset::scriptwrap { #foreach ln $template_lines { #} - set list_input_files [list] - if {$process_extensions eq "ALLFOUNDORCONFIGURED"} { - #todo - look for .wrapconfig or all extensions for the scriptset - puts stderr "Sorry - only single input file supported. Supply a file extension or use a .wrapconfig with a single input file for now - implementation incomplete" + if {[llength $list_input_files] > 1} { + #todo + puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" return false - } else { - lappend list_input_files $scriptroot/$scriptset.$ext } #todo - split template at each etc marker and build a dict of parts @@ -1117,7 +1278,6 @@ namespace eval punk::mix::commandset::scriptwrap { #hack - process one input set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] fconfigure $fdscript -translation binary set script_data [read $fdscript] @@ -1131,7 +1291,8 @@ namespace eval punk::mix::commandset::scriptwrap { } puts stdout "-----------------------------------------------\n" puts stdout "Target for above script data is '$output_file'" - set lang [dict get $extension_langs [string tolower $ext]] + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] puts stdout "Language of script being wrapped is $lang" if {$opt_askme} { set answer [util::askuser "Does this look correct? Y|N"] diff --git a/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/bootsupport/modules/punk/ns-0.1.0.tm index 6bd826e2..f8e55b02 100644 --- a/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -444,9 +444,8 @@ tcl::namespace::eval punk::ns { set nspath [string map {:::: ::} $nspath] set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] - if {[lindex $parts end] eq ""} { - - } + #if {[lindex $parts end] eq ""} { + #} return $parts } @@ -531,6 +530,21 @@ tcl::namespace::eval punk::ns { return [regexp [dict get $ns_re_cache $glob] $path] } + #namespace tree without globbing or weird ns consideration + proc nstree_raw {{location ::}} { + if {![string match ::* $location]} { + error "nstree_raw requires a fully qualified namespace" + } + nstree_rawlist $location + } + proc nstree_rawlist {location} { + set nslist [list $location] + foreach ch [::namespace children $location] { + lappend nslist {*}[nstree_rawlist $ch] + } + return $nslist + } + proc nstree {{location ""}} { if {![string match ::* $location]} { set nscaller [uplevel 1 {::namespace current}] @@ -3899,6 +3913,7 @@ tcl::namespace::eval punk::ns { } proc _pkguse_vars {varnames} { + #review - obsolete? while {"pkguse_vars_[incr n]" in $varnames} {} #return [concat $varnames pkguse_vars_$n] return [list {*}$varnames pkguse_vars_$n] @@ -3932,10 +3947,12 @@ tcl::namespace::eval punk::ns { #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically + variable pkguse_package_to_namespace [dict create] proc pkguse {args} { + variable pkguse_package_to_namespace set argd [punk::args::parse $args withid ::punk::ns::pkguse] lassign [dict values $argd] leaders opts values received - puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + #puts stderr "leaders:$leaders opts:$opts values:$values received:$received" set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] if {[dict exists $received script]} { @@ -3967,68 +3984,159 @@ tcl::namespace::eval punk::ns { set ver "";# tcl version? } default { - if {[string match ::* $pkg_or_existing_ns]} { - set pkg_unqualified [string range $pkg_or_existing_ns 2 end] - if {![tcl::namespace::exists $pkg_or_existing_ns]} { - set ver [package require $pkg_unqualified] - } else { - set ver "" - } + #- comparing namespaces_before vs namespaces_after only works if the package was not previously loaded + #we could either go to the somewhat expensive route of steaming up an interp with the same auto_path & tcl::tm::list each time.. + #or cache the result of the namespace we picked for later pkguse calls (pkguse_package_to_namespace dict) + #we are using the cache method - but this also doesn't help for packages previously loaded by normal package require + #our aim is for pkguse to be deterministic in what namespace it finds - even if it doesn't always get the ideal one (e.g cookiejar, see below) + #To determine appropriate namespace for already loaded packages where we have no cache entry - we may still need the helper interp mechanism + #The helper interp could be persistent - but only so long as the auto_path/tcl::tm::list values are in sync + #review. + + #also see img::png img::raw etc + #these don't directly load namespaces or direct commands.. just change behaviour of existing commands? + #but they can load things like tk (ttk namespace) first one creates ::tkimg? + + if {[string match ::* $pkg_or_existing_ns] && [tcl::namespace::exists $pkg_or_existing_ns]} { + #pkguse on an existing full qualified namespace does no package require set ns $pkg_or_existing_ns + set ver "" } else { - set pkg_unqualified $pkg_or_existing_ns - set ver [package require $pkg_unqualified] - set ns ::$pkg_unqualified - } - #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index - set previous_command_count 0 - if {[namespace exists $ns]} { - set previous_command_count [llength [info commands ${ns}::*]] - } + if {[string match ::* $pkg_or_existing_ns]} { + set pkg_unqualified [string range $pkg_or_existing_ns 2 end] + } else { + set pkg_unqualified $pkg_or_existing_ns + } + #foreach equiv of while 1 - just to allow early exit with break + foreach code_block single { + if {[dict exists $pkguse_package_to_namespace $pkg_unqualified]} { + set ns [dict get $pkguse_package_to_namespace $pkg_unqualified] + set ver [package provide $pkg_unqualified] + break + } + if {[package provide $pkg_unqualified] ne ""} { + #package has already been loaded + if {[namespace exists ::$pkg_unqualified]} { + set ns ::$pkg_unqualified + set ver [package provide $pkg_unqualified] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + break + } + #existing package but no matching namespace.. + #- load in throwaway interp and see what cmds/namespaces created + interp create nstest + try { + nstest eval {tcl::tm::remove {*}[tcl::tm::list]} + nstest eval [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] + nstest eval [list set ::auto_path $::auto_path] + nstest eval {package require punk::ns} + set ns "" + if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] + set ns [nstest eval $script] + } else { + puts "couldn't test pkg $pkg_unqualified\n$errMsg" + } + } finally { + interp delete nstest + } - #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands - #for the purposes of pkguse - which most commonly interactive - we want the namespace populated - #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + set ver [package provide $pkg_unqualified] + break + } - if {!$ns_populated} { - #we will catch-run an auto_index entry if any - #auto_index entry may or may not be prefixed with :: - set keys [list] - #first look for exact pkg_unqualified and ::pkg_unqualified - #leave these at beginning of keys list - if {[array exists ::auto_index($pkg_unqualified)]} { - lappend keys $pkg_unqualified - } - if {[array exists ::auto_index(::$pkg_unqualified)]} { - lappend keys ::$pkg_unqualified - } - #as auto_index is an array - we could get keys in arbitrary order - set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] - lappend keys {*}$matches - set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] - lappend keys {*}$matches - set ns_populated 0 - set i 0 - set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing - set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] - while {!$ns_populated && $i < [llength $keys]} { - #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base - #e.g if we are loading ::x::y - #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc - set k [lindex $keys $i] - set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] - if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { - set auto_source [set ::auto_index($k)] - if {$auto_source ni $already_sourced} { - uplevel 1 $auto_source - lappend already_sourced $auto_source - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + #pkg not loaded + set namespaces_before [nstree_rawlist ::] ;#approx 1ms for 500 or so namespaces - not cheap but bearable + #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index + #gathering prior cmdcount for every ns in system is also a somewhat expensive operation.. review + #we don't know for sure that the namespace for the package require operation actually matches the package name + #e.g tcllib inifile package uses namespace ::ini + #e.g sqlite3 package adds commands to the global namespace + set dict_ns_commandcounts [dict create] + foreach nsb $namespaces_before { + dict set dict_ns_commandcounts $nsb [llength [info commands ${nsb}::*]] + } + + set ver [package require $pkg_unqualified] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + set namespaces_after [nstree_rawlist ::] + + if {[llength $namespaces_after] > [llength $namespaces_before]} { + set namespaces_new [struct::set difference $namespaces_after $namespaces_before] + if {$ns ni $namespaces_new} { + #todo - use shortest result? what if this is a namespace from a required sub package? + #e.g cookiejar loads sqlite3,http,tcl::idna which creates ::sqlite3 etc - but cookiejar just creates an object at ::http::cookiejar + #In this specific case we end up in oo::ObjXXX - but would be better placed in ::http, where the new cookiejar command resides + #review - todo? + set pkgs [package names] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + #find something new - that doesn't match another package name + foreach new $namespaces_new { + if {[lsearch $pkgs [string trimleft $new :]] == -1} { + set ns $new + break + } + } } } - incr i - } + if {[tcl::namespace::exists $ns]} { + #review - only cache if exists? + dict set pkguse_package_to_namespace $pkg_unqualified $ns; + } + set previous_command_count 0 + if {[dict exists $dict_ns_commandcounts $ns]} { + set previous_command_count [dict get $dict_ns_commandcounts $ns] + } + + #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands + #for the purposes of pkguse - which most commonly interactive - we want the namespace populated + #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + + if {!$ns_populated} { + #we will catch-run an auto_index entry if any + #auto_index entry may or may not be prefixed with :: + set keys [list] + #first look for exact pkg_unqualified and ::pkg_unqualified + #leave these at beginning of keys list + if {[array exists ::auto_index($pkg_unqualified)]} { + lappend keys $pkg_unqualified + } + if {[array exists ::auto_index(::$pkg_unqualified)]} { + lappend keys ::$pkg_unqualified + } + #as auto_index is an array - we could get keys in arbitrary order + set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] + lappend keys {*}$matches + set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] + lappend keys {*}$matches + set ns_populated 0 + set i 0 + set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing + set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] + while {!$ns_populated && $i < [llength $keys]} { + #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base + #e.g if we are loading ::x::y + #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc + set k [lindex $keys $i] + set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] + if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { + set auto_source [set ::auto_index($k)] + if {$auto_source ni $already_sourced} { + puts stderr "pkguse sourcing auto_index script $auto_source" + uplevel 1 $auto_source + lappend already_sourced $auto_source + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + } + } + incr i + } + + } + + }; # end foreach code_block single - scope for use of 'break' } } diff --git a/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/bootsupport/modules/punk/repl-0.1.2.tm index a31e255e..fd84ec8d 100644 --- a/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -3567,7 +3567,6 @@ namespace eval repl { if {[catch { package require punk::args - catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical package require punk::config package require punk::ns #puts stderr "loading natsort" @@ -3589,6 +3588,7 @@ namespace eval repl { }} [punk::config::configure running] package require textblock + catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" diff --git a/src/bootsupport/modules/textblock-0.1.3.tm b/src/bootsupport/modules/textblock-0.1.3.tm index 472edc54..f2f4a3af 100644 --- a/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/bootsupport/modules/textblock-0.1.3.tm @@ -6007,7 +6007,7 @@ tcl::namespace::eval textblock { proc welcome_test {} { package require punk::ansi - set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] + set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/ROY-WELC.ANS 80x8]] # Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] diff --git a/src/lib/app-shellspy/shellspy.tcl b/src/lib/app-shellspy/shellspy.tcl index 95f057bb..0508bafe 100644 --- a/src/lib/app-shellspy/shellspy.tcl +++ b/src/lib/app-shellspy/shellspy.tcl @@ -88,11 +88,24 @@ namespace eval shellspy { return [expr {[clock millis]/1000.0}] } variable shellspy_status_log "shellspy-[clock micros]" - set debug_syslog_server 127.0.0.1:514 - #set debug_syslog_server 172.16.6.42:51500 - #set debug_syslog_server "" - set error_syslog_server 127.0.0.1:514 - set data_syslog_server 127.0.0.1:514 + + #todo - default to no logging not even to local syslog + #load a .toml config which can configure logging as desired + set do_log 0 + if {$do_log} { + set debug_syslog_server 127.0.0.1:514 + #set debug_syslog_server 172.16.6.42:51500 + #set debug_syslog_server "" + set error_syslog_server 127.0.0.1:514 + set data_syslog_server 127.0.0.1:514 + } else { + set debug_syslog_server "" + set error_syslog_server "" + set data_syslog_server "" + } + + + shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog $debug_syslog_server -file ""] shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" @@ -570,8 +583,9 @@ namespace eval shellspy { proc do_script_process {scriptbin scriptname args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_script_process got scriptname:'$scriptname' args:'$args'" - set args [do_callback script_process {*}$args] - set params [do_callback_parameters script_process] + #no script_process callbacks + #set args [do_callback script_process {*}$args] + #set params [do_callback_parameters script_process] dict set params -teehandle shellspy set params [dict merge $params [get_channel_config $::testconfig]] @@ -620,7 +634,7 @@ namespace eval shellspy { proc do_script {scriptname replwhen args} { #ideally we don't want to launch an external process to run the script variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" + #shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" set exepath [file dirname [file join [info nameofexecutable] __dummy__]] set exedir [file dirname $exepath] @@ -651,7 +665,7 @@ namespace eval shellspy { set modulesdir $basedir/modules set script [string map [list %a% $args %s% $scriptpath %m% $modulesdir] { -::tcl::tm::add %m% +#::tcl::tm::add %m% set scriptname %s% set normscript [file normalize $scriptname] @@ -696,9 +710,10 @@ dict with prevglobal {} #just the script } + #no script callbacks + #set args [do_callback script {*}$args] + #set params [do_callback_parameters script] - set args [do_callback script {*}$args] - set params [do_callback_parameters script] dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy #dict set params -teehandle punksh @@ -716,7 +731,8 @@ dict with prevglobal {} # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" #} - shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" + #jjj + #shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" if {[dict exists $exitinfo errorInfo]} { #strip out the irrelevant info from the errorInfo - we don't want info beyond 'invoked from within' as this is just plumbing related to the script sourcing set stacktrace [string map [list \r\n \n] [dict get $exitinfo errorInfo]] @@ -730,7 +746,8 @@ dict with prevglobal {} } set output [string trimright $output \n] dict set exitinfo errorInfo $output - shellfilter::log::write $shellspy_status_log "do_script simplified exitinfo: $exitinfo" + #jjj + #shellfilter::log::write $shellspy_status_log "do_script simplified exitinfo: $exitinfo" } return $exitinfo } diff --git a/src/make.tcl b/src/make.tcl index 2de13afb..37f36a9a 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -22,7 +22,7 @@ namespace eval ::punkboot { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /? -h] - variable known_commands [list project modules vfs info check shell vendorupdate bootsupport vfscommonupdate ] + variable known_commands [list project modules libs packages vfs bin info check shell vendorupdate bootsupport vfscommonupdate ] } @@ -1077,10 +1077,16 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - This help." \n \n append h " $scriptname project ?-k?" \n append h " - this is the literal word project - and confirms you want to run the project build - which includes src/vfs/* checks and builds" \n - append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n - append h " - built modules go into /modules /lib etc." \n \n + append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n + append h " - builds/copies .tm modules from src to /modules etc and pkgIndex.tcl based libraries from src to /lib etc." \n \n append h " $scriptname modules" \n - append h " - build modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " - build (or copy if build not required) .tm modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname libs" \n + append h " - build (or copy if build not required) pkgIndex.tcl based libraries from src/lib src/vendorlib etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname packages" \n + append h " - build (or copy if build not required) both .tm and pkgIndex.tcl based packages from src to their corresponding locations under " \n append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n @@ -1089,6 +1095,7 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - bootsupport modules are available to make.tcl" \n \n append h " $scriptname vendorupdate" \n append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n + append h " - update the src/vendorlib based on src/vendorlib/config.toml (todo)" \n \n append h " $scriptname vfscommonupdate" \n append h " - update the src/vfs/_vfscommon.vfs from compiled src/modules and src/lib etc" \n append h " - before calling this (followed by make project) - you can test using '(.exe) dev'" \n @@ -1213,6 +1220,8 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} } set sourcefolder $projectroot/src +set binfolder $projectroot/bin + if {$::punkboot::command eq "check"} { set sep [string repeat - 75] puts stdout $sep @@ -1348,8 +1357,8 @@ if {$::punkboot::command eq "info"} { puts stdout "- -- --- --- --- --- --- --- --- --- -- -" puts stdout "- projectroot : $projectroot" set sourcefolder $projectroot/src - set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] - set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] + set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] + set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] puts stdout "- vendorlib folders: ([llength $vendorlibfolders])" foreach fld $vendorlibfolders { puts stdout " src/$fld" @@ -1358,13 +1367,18 @@ if {$::punkboot::command eq "info"} { foreach fld $vendormodulefolders { puts stdout " src/$fld" } - set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] - puts stdout "- source module paths: [llength $source_module_folderlist]" - foreach fld $source_module_folderlist { + #set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] ;#returns only those containing .tm files + #foreach fld $source_module_folderlist { + # set relpath [punkcheck::lib::path_relative $projectroot $fld] + # puts stdout " $relpath" + #} + set projectmodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails modules_tcl* modules] + puts stdout "- source module paths: [llength $projectmodulefolders]" + #JJJ + foreach fld $projectmodulefolders { puts stdout " $fld" } - set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] - lappend projectlibfolders lib + set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl* lib] puts stdout "- source libary paths: [llength $projectlibfolders]" foreach fld $projectlibfolders { puts stdout " src/$fld" @@ -1759,7 +1773,7 @@ if {$::punkboot::command eq "bootsupport"} { -if {$::punkboot::command ni {project modules vfs}} { +if {$::punkboot::command ni {project modules libs packages vfs bin}} { puts stderr "Command $::punkboot::command not implemented - aborting." flush stderr after 100 @@ -1772,7 +1786,7 @@ if {$::punkboot::command ni {project modules vfs}} { #install src vendor contents (from version controlled src folder) to base of project (same target folders as our own src/modules etc ie to paths that go on the auto_path and in tcl::tm::list) -if {$::punkboot::command in {project modules}} { +if {$::punkboot::command in {project packages modules}} { set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] foreach vf $vendormodulefolders { lassign [split $vf _] _vm tclx @@ -1797,7 +1811,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendormodulefolders]} { puts stderr "VENDORMODULES: No src/vendormodules or src/vendormodules_tcl* folders found." } +} +if {$::punkboot::command in {project packages libs}} { set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] foreach lf $vendorlibfolders { lassign [split $lf _] _vm tclx @@ -1827,8 +1843,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendorlibfolders]} { puts stderr "VENDORLIB: No src/vendorlib or src/vendorlib_tcl* folder found." } +} - +if {$::punkboot::command in {project packages modules libs}} { ######################################################## #templates #e.g The default project layout is mainly folder structure and readme files - but has some scripts developed under the main src that we want to sync @@ -1896,6 +1913,9 @@ if {$::punkboot::command in {project modules}} { $tpl_installer destroy } } +} + +if {$::punkboot::command in {project packages libs}} { ######################################################## set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] lappend projectlibfolders lib @@ -1927,7 +1947,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $projectlibfolders]} { puts stderr "PROJECTLIB: No src/lib or src/lib_tcl* folder found." } +} +if {$::punkboot::command in {project packages modules}} { #consolidated /modules /modules_tclX folder used for target where X is tcl major version #the make process will process for any _tclX not just the major version of the current interpreter @@ -1964,9 +1986,10 @@ if {$::punkboot::command in {project modules}} { ] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } +} +if {$::punkboot::command in {project packages modules libs}} { set installername "make.tcl" - # ---------------------------------------- if {[punk::repo::is_fossil_root $projectroot]} { set config [dict create\ @@ -2013,7 +2036,7 @@ if {$::punkboot::command in {project modules}} { #review set installername "make.tcl" -if {$::punkboot::command ni {project vfs}} { +if {$::punkboot::command ni {project vfs bin}} { #command = modules puts stdout "vfs folders not checked" puts stdout " - use 'make.tcl vfscommonupdate' to copy built modules into base vfs folder" @@ -2033,6 +2056,17 @@ if {$buildfolder ne "$sourcefolder/_build"} { exit 2 } +if {$::punkboot::command eq "bin"} { + puts stdout "checking $sourcefolder/bin" + set resultdict [punkcheck::install $sourcefolder/bin $binfolder\ + -overwrite synced-targets\ + -installer "punkboot-bin"\ + -progresschannel stdout\ + ] + + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + flush stdout +} #find runtimes set rtfolder $sourcefolder/runtime @@ -2056,11 +2090,32 @@ if {![llength $runtimes]} { } set has_sdx 1 -if {[catch {exec sdx help} errM]} { - puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" - puts stderr "err: $errM" - #exit 1 - set has_sdx 0 +set sdxpath [auto_execok $binfolder/sdx] +if {$sdxpath eq ""} { + set sdxpath [auto_execok [file dirname [info nameofexecutable]]/sdx] + if {$sdxpath eq ""} { + #last resort - look on path + set sdxpath [auto_execok sdx] + } + if {$sdxpath eq ""} { + #last resort - a tclkit and sdx.kit fine + if {[file exists $binfolder/sdx.kit]} { + set tclkitpath [auto_execok $binfolder/tclkit] + if {$tclkitpath eq ""} { + set tclkitpath [auto_execok tclkit] + } + set sdxpath [list {*}$tclkitpath $binfolder/sdx.kit] + } + } + + if {$sdxpath eq "" || [catch {exec {*}$sdxpath help} errM]} { + puts stderr "FAILED to find usable sdx command or tclkit executable with sdx.bat" + puts stderr "If tclkit-based runtimes are required - check that sdx executable is in bin folder of project or in same folder as tcl/punk executable or on path" + puts stderr "This is not a problem if tcl8.7/tcl9+ kits using the preferred method 'zipfs' are to be used, or if cookfs based kits are to be used." + puts stderr "err: $errM" + #exit 1 + set has_sdx 0 + } } # -- --- --- --- --- --- --- --- --- --- @@ -2825,17 +2880,17 @@ foreach vfstail $vfs_tails { if {[catch { if {$rtname ne "-"} { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose } else { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" } else { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } - puts stderr "sdx wrap $targetkit failed" + puts stderr "$::sdxpath wrap $targetkit failed" lappend failed_kits [list kit $targetkit reason $sdxmsg] $vfs_event targetset_end FAILED $vfs_event destroy @@ -3022,6 +3077,7 @@ foreach vfstail $vfs_tails { } ;#end foreach rtname in runtimes # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- } + cd $startdir if {[llength $installed_kits]} { puts stdout "INSTALLED KITS: ([llength $installed_kits])" diff --git a/src/modules/punk/libunknown-0.1.tm b/src/modules/punk/libunknown-0.1.tm index a4f56010..1b15d45a 100644 --- a/src/modules/punk/libunknown-0.1.tm +++ b/src/modules/punk/libunknown-0.1.tm @@ -890,10 +890,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - puts stderr "--> pkg epoch $prev_e -> $current_e" - puts stderr "args: $args" - puts stderr "last_auto: $last_auto_path" - puts stderr "auto_path: $auto_path" + #puts stderr "--> pkg epoch $prev_e -> $current_e" + #puts stderr "args: $args" + #puts stderr "last_auto: $last_auto_path" + #puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries diff --git a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm index 5e1d19db..8a58f78e 100644 --- a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm @@ -20,7 +20,7 @@ #[manpage_begin punkshell_module_scriptwrap 0 999999.0a1.0] #[copyright "2024"] #[titledesc {scriptwrap polyglot tool}] [comment {-- Name section and table of contents description --}] -#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] +#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] #[require punk::mix::commandset::scriptwrap] #[keywords module commandset launcher scriptwrap] #[description] @@ -30,7 +30,7 @@ #*** !doctools #[section Overview] -#[para] overview of scriptwrap +#[para] overview of scriptwrap #[subsection Concepts] #[para] - @@ -74,7 +74,7 @@ package require punk::fileline namespace eval punk::mix::commandset::scriptwrap { #*** !doctools #[subsection {Namespace punk::mix::commandset::scriptwrap}] - #[para] Core API functions for punk::mix::commandset::scriptwrap + #[para] Core API functions for punk::mix::commandset::scriptwrap #[list_begin definitions] namespace export * @@ -93,7 +93,7 @@ namespace eval punk::mix::commandset::scriptwrap { foreach k [lreverse [dict keys $tdict_low_to_high]] { dict set tdict $k [dict get $tdict_low_to_high $k] } - + #set pathinfolist [dict values $tdict] set names [dict keys $tdict] @@ -142,9 +142,9 @@ namespace eval punk::mix::commandset::scriptwrap { put stderr "commandset::scriptwrap::templates_dict WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } return - } - - + } + + #A batch file with unix line-endings is sensitive to label positioning. #batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it. #see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings) @@ -808,176 +808,317 @@ namespace eval punk::mix::commandset::scriptwrap { return $result } #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site - #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf - proc multishell {filepath_or_scriptset args} { - set opts [dict create\ - -askme 1\ - -outputfolder "\uFFFF"\ - -template "\uFFFF"\ - -returnextra 0\ - -force 0\ - ] - #set known_opts [dict keys $defaults] - foreach {k v} $args { - switch -- $k { - -askme - -outputfolder - -template - -returnextra - -force { - dict set opts $k $v - } - default { - error "punk::mix::commandset::multishell error. Unrecognized option '$k'. Known-options: [dict keys $opts]" - } + #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf + #set usage "" + #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n + #append usage "The scriptset name will be used to search for .sh|.tcl|.ps1 or names as you specify in yourname.wrapconfig if it exists" \n + #append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n + #if {![string length $filepath_or_scriptset]} { + # puts stderr "No filepath_or_scriptset specified" + # puts stderr $usage + # return false + #} + proc _read_scriptset_wrap_tomlfile {fname} { + set resultd [dict create] + package require tomlish + set tomldata [readFile $fname] + #todo - fix tomlish to provide line number in ERROR structure during from_toml call. + if {[catch {tomlish::to_dict [tomlish::from_toml $tomldata]} tomldict]} { + puts stderr "Failed to parse $fname" + puts stderr "error: $tomldict" + } + if {[tomlish::dict::path::exists $tomldict {.application.template}]} { + dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] + } + set scripts [list] + if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { + set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] + foreach tvdict $arrvalues { + lappend scripts [dict get $tvdict value] } } + dict set resultd scripts $scripts - set usage "" - append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n - append usage "The scriptset name will be used to search for yourname.sh|tcl|ps1 or names as you specify in yourname.wrapconfig if it exists" \n - append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n - if {![string length $filepath_or_scriptset]} { - puts stderr "No filepath_or_scriptset specified" - puts stderr $usage - return false + set ftail [file rootname [file tail $fname]] ;#e.g example_wrap.toml + set scriptset [lindex [split $ftail _] 0] + set fallback_outputfile $scriptset.cmd + set fallback_nextshellpath "/usr/bin/env tclsh" + set fallback_nextshelltype "tcl" + + if {[tomlish::dict::path::exists $tomldict {.application.default_outputfile.value}]} { + dict set resultd default_outputfile [tomlish::dict::path::get $tomldict {.application.default_outputfile.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshellpath.value}]} { + dict set resultd default_nextshellpath [tomlish::dict::path::get $tomldict {.application.default_nextshellpath.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshelltype.value}]} { + dict set resultd default_nextshelltype [tomlish::dict::path::get $tomldict {.application.default_nextshelltype.value}] + } + foreach platform {win32 dragonflybsd freebsd netbsd linux macosx other} { + set d [dict create] + foreach field {outputfile nextshellpath nextshelltype} { + if {[tomlish::dict::path::exists $tomldict ".application.$platform.$field.value"]} { + dict set d $field [tomlish::dict::path::get $tomldict ".application.$platform.$field.value"] + } else { + if {[dict exists $resultd default_$field]} { + dict set d $field [dict get $resultd default_$field] + } else { + dict set d $field [set fallback_$field] + } + } + } + dict set resultd $platform $d } + + return $resultd + } + punk::args::define { + @id -id ::punk::mix::commandset::scriptwrap::multishell + @cmd -name punk::mix::commandset::scriptwrap::multishell\ + -summary\ + "Wrap script(s) into a polyglot cross-platform executable script."\ + -help\ + "Create a polyglot executable script that wraps constituent scripts written in + various scripting languages such as perl, tcl, shell script, powershell. + The resulting polyglot file should run cross platform on windows and various + types of unix-like OS. For use on windows the output file should be named with + a .bat or .cmd extension - but the same file with extension removed should also + be capable of running on FreeBSD, Linux etc. + Note that a polyglot script such as this may be somewhat brittle over the long + term with regards to default shells and scripting languages across platforms." + @leaders -min 1 -max 1 + filepath_or_scriptset -type string -minsize 1 -help\ + "Supply the path to a single script file to wrap, or the name of a scriptset. + The scriptset name will be used to search for .sh|.bash|.tcl|.ps1|.pl + or alternatively, names as specified in a configuration file named _wrap.toml + if it exists in the current folder, or is specified with a full path name. + If no template name/path is specified in a _wrap.toml file and no + -template argument is supplied the default punk.multishell.cmd will be used. + If the template is specified explicitly in -template as well as in the .toml + file - the supplied -template argument will override that specified in the + .toml file." + @opts + -template -type string -default "punk.multishell.cmd" -help\ + "Templates are provided from modules or paths in the current project, + so available templates will vary based on whether the multishell + command is being run from within a project directory or not. + To see available templates use punk::mix::commandset::scriptwrap::templates." + -outputfolder -type directory -default "" -help\ + "Folder to which to write resulting polyglot script. + If empty, the output will go to the /bin folder or + to the current working directory if there is no projectroot." + -askme -type boolean -default 1 -help\ + "Prompt user at console (stdin) for confirmation of operations such as + overwrite." + -force -type boolean -default 0 + -returnextra -type boolean -default 0 + @values -minvalues 0 -maxvalues 0 + } + #: + #@SET "nextshellpath[win32___________]=tclsh___________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[other___________]=tcl_____________" + #: + proc multishell {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] + lassign [dict values $argd] leaders opts values received + # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_askme [dict get $opts -askme] - set opt_template [dict get $opts -template] - set opt_outputfolder [dict get $opts -outputfolder] - set opt_returnextra [dict get $opts -returnextra] - set opt_force [dict get $opts -force] + set filepath_or_scriptset [dict get $leaders filepath_or_scriptset] + set opt_askme [dict get $opts -askme] + set opt_template [dict get $opts -template] ;#use dict exists $received -template to see if overridable in .toml + set opt_outputfolder [dict get $opts -outputfolder] + set opt_returnextra [dict get $opts -returnextra] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- - set ext [file extension $filepath_or_scriptset] set startdir [pwd] + set allowed_extensions [list tcl ps1 sh bash pl] + #TODO - distinct sections for sh vs bash? needs experiments.. + #for now we use shell-pre-launch-subprocess etc + #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] + set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] + + if {[file pathtype $filepath_or_scriptset] ni {absolute relative}} { + error "bad pathtype for '$filepath_or_scriptset' (expected absolute or relative path, or name of scriptset)" + } - - - #first check if relative or absolute path matches a file + #first check if absolute path matches a file or relative path from cwd matches a file if {[file pathtype $filepath_or_scriptset] eq "absolute"} { - set specified_path $filepath_or_scriptset + set specified_path $filepath_or_scriptset } else { set specified_path [file join $startdir $filepath_or_scriptset] } + set scriptdir [file dirname $specified_path] + set ext [string trim [file extension $filepath_or_scriptset] .] - set allowed_extensions [list wrapconfig tcl ps1 sh bash pl] - set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] - #set allowed_extensions [list tcl] - set found_script 0 - if {[file exists $specified_path]} { - set found_script 1 + set scriptset "" + if {$ext eq ""} { + set scriptset [file rootname [file tail $specified_path]] + } elseif {$ext eq "toml"} { + set tomltail [file tail $specified_path] + if {[string match *_wrap.toml $tomltail]} { + set scriptset [lindex [split $tomltail _] 0] + #if .toml was specified - the config file must exist + if {![file exists $specified_path]} { + if {[file pathtype $filepath_or_scriptset] eq "relative"} { + puts stderr "unable to locate '$specified_path' - will continue search in src/scriptapps folder" + } else { + #caller was specific about path - no fallback to src/scriptapps + error "unable to locate '$specified_path'" + } + } + } else { + error "supplied toml file must be of form _wrap.toml" + } } else { - foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { - if {[file exists $filepath_or_scriptset.$e]} { - set found_script 1 - break + if {$ext ni $allowed_extensions} { + error "supplied filepath_or_scriptset must be the name of a scriptset without extension, a file named _wrap.toml, or a script with one of the extensions: $allowed_extensions" + } + } + + set list_input_files [list] + set configd [dict create] + if {$scriptset ne ""} { + puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" + #.toml file may or may not exist + if {[file exists ${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptdir $s] + } + } + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" + return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptdir" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptdir/$scriptset.$e]} { + lappend list_input_files $scriptdir/$scriptset.$e + } } } + } else { + #expect a single script + if {[file exists $specified_path]} { + lappend list_input_files $specified_path + } } + set found_script [expr {[llength $list_input_files] > 0}] #TODO! - use get_wrapper_folders - multishell should use same available templates as the 'templates' function - set scriptset [file rootname [file tail $specified_path]] if {$found_script} { - if {[file type $specified_path] eq "file"} { - set specified_root [file dirname $specified_path] - set pathinfo [punk::repo::find_repos [file dirname $specified_path]] - set projectroot [dict get $pathinfo closest] + #found scripts at absolute path - or path relative to cwd + set scriptroot $scriptdir + set pathinfo [punk::repo::find_repos $scriptroot] + set projectroot [dict get $pathinfo closest] + if {[file exists $scriptroot/wrappers]} { + set customwrapper_folder $scriptroot/wrappers + } else { + #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder if {[string length $projectroot]} { - #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - set customwrapper_folder $projectroot/src/scriptapps/wrappers - } + set customwrapper_folder $projectroot/src/scriptapps/wrappers } else { #outside of any project - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - #no customwrapper folder available - set customwrapper_folder "" - } + set customwrapper_folder "" } - } else { - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false } } else { + if {[file pathtype $filepath_or_scriptset] eq "absolute"} { + return false + } set pathinfo [punk::repo::find_repos $startdir] set projectroot [dict get $pathinfo closest] - if {[string length $projectroot]} { - if {[llength [file split $filepath_or_scriptset]] > 1} { - puts stderr "filepath_or_scriptset looks like a path - but doesn't seem to point to a file" - puts stderr "Ensure you are within a project and use just the name of the scriptset, or pass in the full correct path or relative path to current directory" - puts stderr $usage - return false - } else { - #we've already ruled out empty string - so must have a single element representing scriptset - possibly with file extension - set scriptroot $projectroot/src/scriptapps - set customwrapper_folder $projectroot/src/scriptapps/wrappers - #check something matches the scriptset.. - set something_found "" - if {[file exists $scriptroot/$scriptset]} { - set found_script 1 - set something_found $scriptroot/$scriptset ;#extensionless file - that's ok too - } else { - foreach e $allowed_extensions { - if {[file exists $scriptroot/$scriptset.$e]} { - set found_script 1 - set something_found $scriptroot/$scriptset.$e - break - } + if {![string length $projectroot]} { + puts stderr "No matching scripts or config found for $filepath_or_scriptset, and you are not within a directory where projectroot and src/scriptapps can be determined" + return false + } + + set scriptroot $projectroot/src/scriptapps + set customwrapper_folder $projectroot/src/scriptapps/wrappers + #check something matches the scriptset.. + if {$scriptset ne ""} { + #.toml file may or may not exist + if {[file exists $scriptroot/${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptroot $s] } } - if {!$found_script} { - puts stderr "Searched within $scriptroot" - puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" - puts stderr $usage + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" return false - } else { - if {[file type $something_found] ne "file"} { - puts stderr "Found '$something_found'" - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptroot" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptroot/$scriptset.$e]} { + lappend list_input_files $scriptroot/$scriptset.$e } } } - } else { - puts stderr "filepath_or_scriptset parameter doesn't seem to refer to a file, and you are not within a directory where projectroot and src/scriptapps/wrappers can be determined" - puts stderr $usage - return false + #expect a single script + if {[file exists $scriptroot/$filepath_or_scriptset]} { + if {[file type $scriptroot/$filepath_or_scriptset] ne "file"} { + puts stderr "wrap_in_multishell doesn't currently support a directory as the path. path: $scriptroot/$filepath_or_scriptset" + return false + } + lappend list_input_files $scriptroot/$filepath_or_scriptset + } } - } - #assertion - customwrapper_folder var exists - but might be empty + set found_script [expr {[llength $list_input_files] > 0}] - - if {[string length $ext]} { - #If there was an explicitly supplied extension - then that file should exist - if {![file exists $scriptroot/$scriptset.$ext]} { - puts stderr "Explicit extension .$ext was supplied - but matching file not found." - puts stderr $usage - return false - } else { - if {$ext eq "wrapconfig"} { - set process_extensions ALLFOUNDORCONFIGURED + #---------------------- + if {!$found_script} { + puts stderr "Searched within $scriptdir and $scriptroot" + if {$scriptset ne ""} { + puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" } else { - set process_extensions $ext + puts stderr "Unable to find file $filepath_or_scriptset" } + return false } - } else { - #no explicit extension - process all for scriptset - set process_extensions ALLFOUNDORCONFIGURED + } - #process_extensions - either a single one - or all found or as per .wrapconfig + #assertion - customwrapper_folder var exists - but might be empty - if {$opt_template eq "\uFFFF"} { - set templatename punk.multishell.cmd + if {[dict exists $configd template]} { + set templatename [dict get $configd template] } else { - set templatename $opt_template + if {$opt_template eq "\uFFFF"} { + set templatename punk.multishell.cmd + } else { + set templatename $opt_template + } } set templatename_root [file rootname [file tail $templatename]] @@ -995,7 +1136,7 @@ namespace eval punk::mix::commandset::scriptwrap { set template_base_dict [punk::mix::base::lib::get_template_basefolders] set tpldirs [list] dict for {tdir tsourceinfo} $template_base_dict { - set vendor [dict get $tsourceinfo vendor] + set vendor [dict get $tsourceinfo vendor] if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { lappend tpldirs $tdir } elseif {[file exists $tdir/utility/scriptappwrappers/${templatename_fileroot}[file extension $templatename]]} { @@ -1032,7 +1173,7 @@ namespace eval punk::mix::commandset::scriptwrap { } - if {$opt_outputfolder eq "\uFFFF"} { + if {$opt_outputfolder eq ""} { #outputfolder not explicitly specified by caller if {[string length $projectroot]} { set output_folder [file join $projectroot/bin] @@ -1056,13 +1197,36 @@ namespace eval punk::mix::commandset::scriptwrap { #todo - #output_file extension may also depend on the template being used.. and/or the .wrapconfig - if {$::tcl_platform(platform) eq "windows"} { - set output_extension cmd + #output_file extension may also depend on the template being used.. and/or the _wrap.toml config + + if {[dict size $configd]} { + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other + } + } + set out [dict get $configd $ptype outputfile] + set output_file [file join $output_folder $out] } else { - set output_extension sh + #no _wrap.toml file available + if {$::tcl_platform(platform) eq "windows"} { + set output_extension .cmd + } else { + set output_extension .sh + } + if {$scriptset ne ""} { + set output_file [file join $output_folder $scriptset$output_extension] + } else { + set infile [lindex $list_input_files 0] + set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] + } } - set output_file [file join $output_folder $scriptset.$output_extension] + + if {[file exists $output_file]} { set fdexisting [open $output_file r] fconfigure $fdexisting -translation binary @@ -1103,13 +1267,10 @@ namespace eval punk::mix::commandset::scriptwrap { #foreach ln $template_lines { #} - set list_input_files [list] - if {$process_extensions eq "ALLFOUNDORCONFIGURED"} { - #todo - look for .wrapconfig or all extensions for the scriptset - puts stderr "Sorry - only single input file supported. Supply a file extension or use a .wrapconfig with a single input file for now - implementation incomplete" + if {[llength $list_input_files] > 1} { + #todo + puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" return false - } else { - lappend list_input_files $scriptroot/$scriptset.$ext } #todo - split template at each etc marker and build a dict of parts @@ -1117,7 +1278,6 @@ namespace eval punk::mix::commandset::scriptwrap { #hack - process one input set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] fconfigure $fdscript -translation binary set script_data [read $fdscript] @@ -1131,7 +1291,8 @@ namespace eval punk::mix::commandset::scriptwrap { } puts stdout "-----------------------------------------------\n" puts stdout "Target for above script data is '$output_file'" - set lang [dict get $extension_langs [string tolower $ext]] + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] puts stdout "Language of script being wrapped is $lang" if {$opt_askme} { set answer [util::askuser "Does this look correct? Y|N"] diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd index 2975975d..9daf7ebf 100644 --- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd @@ -209,6 +209,8 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' SET task_exitcode=66 @REM boundary padding @REM boundary padding + @REM boundary padding + @REM boundary padding GOTO :exit_multishell ) ) @@ -223,7 +225,9 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" - @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" + @REM Although drive letters on windows are normally upper case wslbash seems to expect lower case drive letters + @CALL :stringToLower %drive ldrive + @SET "result=/mnt/%ldrive:~0,1%%_path:\=/%%name%" @ENDLOCAL & ( @if "%~2" neq "" ( SET "%rtrn%=%result%" @@ -336,7 +340,8 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) ) @EXIT /B - +@REM boundary padding +@REM boundary padding :stringToUpper @SETLOCAL @SET "rtrn=%~2" @@ -354,6 +359,25 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) ) @EXIT /B +:stringToLower +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "retstring=%~1" + @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( + @SET "retstring=!retstring:%%A=%%A!" + ) + @SET "result=!retstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToLower %string% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding :stringTrimTrailingUnderscores @SETLOCAL @SET "rtrn=%~2" @@ -397,6 +421,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' :endlib : \ @REM padding +@REM padding @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell @GOTO :exit_multishell # } diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index 0bbf4d5a..022a4a0f 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -444,9 +444,8 @@ tcl::namespace::eval punk::ns { set nspath [string map {:::: ::} $nspath] set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] - if {[lindex $parts end] eq ""} { - - } + #if {[lindex $parts end] eq ""} { + #} return $parts } @@ -531,6 +530,21 @@ tcl::namespace::eval punk::ns { return [regexp [dict get $ns_re_cache $glob] $path] } + #namespace tree without globbing or weird ns consideration + proc nstree_raw {{location ::}} { + if {![string match ::* $location]} { + error "nstree_raw requires a fully qualified namespace" + } + nstree_rawlist $location + } + proc nstree_rawlist {location} { + set nslist [list $location] + foreach ch [::namespace children $location] { + lappend nslist {*}[nstree_rawlist $ch] + } + return $nslist + } + proc nstree {{location ""}} { if {![string match ::* $location]} { set nscaller [uplevel 1 {::namespace current}] @@ -3899,6 +3913,7 @@ tcl::namespace::eval punk::ns { } proc _pkguse_vars {varnames} { + #review - obsolete? while {"pkguse_vars_[incr n]" in $varnames} {} #return [concat $varnames pkguse_vars_$n] return [list {*}$varnames pkguse_vars_$n] @@ -3932,10 +3947,12 @@ tcl::namespace::eval punk::ns { #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically + variable pkguse_package_to_namespace [dict create] proc pkguse {args} { + variable pkguse_package_to_namespace set argd [punk::args::parse $args withid ::punk::ns::pkguse] lassign [dict values $argd] leaders opts values received - puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + #puts stderr "leaders:$leaders opts:$opts values:$values received:$received" set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] if {[dict exists $received script]} { @@ -3967,68 +3984,159 @@ tcl::namespace::eval punk::ns { set ver "";# tcl version? } default { - if {[string match ::* $pkg_or_existing_ns]} { - set pkg_unqualified [string range $pkg_or_existing_ns 2 end] - if {![tcl::namespace::exists $pkg_or_existing_ns]} { - set ver [package require $pkg_unqualified] - } else { - set ver "" - } + #- comparing namespaces_before vs namespaces_after only works if the package was not previously loaded + #we could either go to the somewhat expensive route of steaming up an interp with the same auto_path & tcl::tm::list each time.. + #or cache the result of the namespace we picked for later pkguse calls (pkguse_package_to_namespace dict) + #we are using the cache method - but this also doesn't help for packages previously loaded by normal package require + #our aim is for pkguse to be deterministic in what namespace it finds - even if it doesn't always get the ideal one (e.g cookiejar, see below) + #To determine appropriate namespace for already loaded packages where we have no cache entry - we may still need the helper interp mechanism + #The helper interp could be persistent - but only so long as the auto_path/tcl::tm::list values are in sync + #review. + + #also see img::png img::raw etc + #these don't directly load namespaces or direct commands.. just change behaviour of existing commands? + #but they can load things like tk (ttk namespace) first one creates ::tkimg? + + if {[string match ::* $pkg_or_existing_ns] && [tcl::namespace::exists $pkg_or_existing_ns]} { + #pkguse on an existing full qualified namespace does no package require set ns $pkg_or_existing_ns + set ver "" } else { - set pkg_unqualified $pkg_or_existing_ns - set ver [package require $pkg_unqualified] - set ns ::$pkg_unqualified - } - #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index - set previous_command_count 0 - if {[namespace exists $ns]} { - set previous_command_count [llength [info commands ${ns}::*]] - } + if {[string match ::* $pkg_or_existing_ns]} { + set pkg_unqualified [string range $pkg_or_existing_ns 2 end] + } else { + set pkg_unqualified $pkg_or_existing_ns + } + #foreach equiv of while 1 - just to allow early exit with break + foreach code_block single { + if {[dict exists $pkguse_package_to_namespace $pkg_unqualified]} { + set ns [dict get $pkguse_package_to_namespace $pkg_unqualified] + set ver [package provide $pkg_unqualified] + break + } + if {[package provide $pkg_unqualified] ne ""} { + #package has already been loaded + if {[namespace exists ::$pkg_unqualified]} { + set ns ::$pkg_unqualified + set ver [package provide $pkg_unqualified] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + break + } + #existing package but no matching namespace.. + #- load in throwaway interp and see what cmds/namespaces created + interp create nstest + try { + nstest eval {tcl::tm::remove {*}[tcl::tm::list]} + nstest eval [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] + nstest eval [list set ::auto_path $::auto_path] + nstest eval {package require punk::ns} + set ns "" + if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] + set ns [nstest eval $script] + } else { + puts "couldn't test pkg $pkg_unqualified\n$errMsg" + } + } finally { + interp delete nstest + } - #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands - #for the purposes of pkguse - which most commonly interactive - we want the namespace populated - #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + set ver [package provide $pkg_unqualified] + break + } - if {!$ns_populated} { - #we will catch-run an auto_index entry if any - #auto_index entry may or may not be prefixed with :: - set keys [list] - #first look for exact pkg_unqualified and ::pkg_unqualified - #leave these at beginning of keys list - if {[array exists ::auto_index($pkg_unqualified)]} { - lappend keys $pkg_unqualified - } - if {[array exists ::auto_index(::$pkg_unqualified)]} { - lappend keys ::$pkg_unqualified - } - #as auto_index is an array - we could get keys in arbitrary order - set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] - lappend keys {*}$matches - set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] - lappend keys {*}$matches - set ns_populated 0 - set i 0 - set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing - set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] - while {!$ns_populated && $i < [llength $keys]} { - #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base - #e.g if we are loading ::x::y - #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc - set k [lindex $keys $i] - set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] - if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { - set auto_source [set ::auto_index($k)] - if {$auto_source ni $already_sourced} { - uplevel 1 $auto_source - lappend already_sourced $auto_source - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + #pkg not loaded + set namespaces_before [nstree_rawlist ::] ;#approx 1ms for 500 or so namespaces - not cheap but bearable + #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index + #gathering prior cmdcount for every ns in system is also a somewhat expensive operation.. review + #we don't know for sure that the namespace for the package require operation actually matches the package name + #e.g tcllib inifile package uses namespace ::ini + #e.g sqlite3 package adds commands to the global namespace + set dict_ns_commandcounts [dict create] + foreach nsb $namespaces_before { + dict set dict_ns_commandcounts $nsb [llength [info commands ${nsb}::*]] + } + + set ver [package require $pkg_unqualified] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + set namespaces_after [nstree_rawlist ::] + + if {[llength $namespaces_after] > [llength $namespaces_before]} { + set namespaces_new [struct::set difference $namespaces_after $namespaces_before] + if {$ns ni $namespaces_new} { + #todo - use shortest result? what if this is a namespace from a required sub package? + #e.g cookiejar loads sqlite3,http,tcl::idna which creates ::sqlite3 etc - but cookiejar just creates an object at ::http::cookiejar + #In this specific case we end up in oo::ObjXXX - but would be better placed in ::http, where the new cookiejar command resides + #review - todo? + set pkgs [package names] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + #find something new - that doesn't match another package name + foreach new $namespaces_new { + if {[lsearch $pkgs [string trimleft $new :]] == -1} { + set ns $new + break + } + } } } - incr i - } + if {[tcl::namespace::exists $ns]} { + #review - only cache if exists? + dict set pkguse_package_to_namespace $pkg_unqualified $ns; + } + set previous_command_count 0 + if {[dict exists $dict_ns_commandcounts $ns]} { + set previous_command_count [dict get $dict_ns_commandcounts $ns] + } + + #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands + #for the purposes of pkguse - which most commonly interactive - we want the namespace populated + #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + + if {!$ns_populated} { + #we will catch-run an auto_index entry if any + #auto_index entry may or may not be prefixed with :: + set keys [list] + #first look for exact pkg_unqualified and ::pkg_unqualified + #leave these at beginning of keys list + if {[array exists ::auto_index($pkg_unqualified)]} { + lappend keys $pkg_unqualified + } + if {[array exists ::auto_index(::$pkg_unqualified)]} { + lappend keys ::$pkg_unqualified + } + #as auto_index is an array - we could get keys in arbitrary order + set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] + lappend keys {*}$matches + set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] + lappend keys {*}$matches + set ns_populated 0 + set i 0 + set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing + set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] + while {!$ns_populated && $i < [llength $keys]} { + #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base + #e.g if we are loading ::x::y + #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc + set k [lindex $keys $i] + set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] + if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { + set auto_source [set ::auto_index($k)] + if {$auto_source ni $already_sourced} { + puts stderr "pkguse sourcing auto_index script $auto_source" + uplevel 1 $auto_source + lappend already_sourced $auto_source + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + } + } + incr i + } + + } + + }; # end foreach code_block single - scope for use of 'break' } } diff --git a/src/modules/punk/repl-999999.0a1.0.tm b/src/modules/punk/repl-999999.0a1.0.tm index 53dc3153..25ecd92a 100644 --- a/src/modules/punk/repl-999999.0a1.0.tm +++ b/src/modules/punk/repl-999999.0a1.0.tm @@ -3567,7 +3567,6 @@ namespace eval repl { if {[catch { package require punk::args - catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical package require punk::config package require punk::ns #puts stderr "loading natsort" @@ -3589,6 +3588,7 @@ namespace eval repl { }} [punk::config::configure running] package require textblock + catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" diff --git a/src/modules/textblock-999999.0a1.0.tm b/src/modules/textblock-999999.0a1.0.tm index ae286f36..c7e44294 100644 --- a/src/modules/textblock-999999.0a1.0.tm +++ b/src/modules/textblock-999999.0a1.0.tm @@ -6007,7 +6007,7 @@ tcl::namespace::eval textblock { proc welcome_test {} { package require punk::ansi - set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] + set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/ROY-WELC.ANS 80x8]] # Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index 2de13afb..37f36a9a 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.basic/src/make.tcl @@ -22,7 +22,7 @@ namespace eval ::punkboot { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /? -h] - variable known_commands [list project modules vfs info check shell vendorupdate bootsupport vfscommonupdate ] + variable known_commands [list project modules libs packages vfs bin info check shell vendorupdate bootsupport vfscommonupdate ] } @@ -1077,10 +1077,16 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - This help." \n \n append h " $scriptname project ?-k?" \n append h " - this is the literal word project - and confirms you want to run the project build - which includes src/vfs/* checks and builds" \n - append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n - append h " - built modules go into /modules /lib etc." \n \n + append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n + append h " - builds/copies .tm modules from src to /modules etc and pkgIndex.tcl based libraries from src to /lib etc." \n \n append h " $scriptname modules" \n - append h " - build modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " - build (or copy if build not required) .tm modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname libs" \n + append h " - build (or copy if build not required) pkgIndex.tcl based libraries from src/lib src/vendorlib etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname packages" \n + append h " - build (or copy if build not required) both .tm and pkgIndex.tcl based packages from src to their corresponding locations under " \n append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n @@ -1089,6 +1095,7 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - bootsupport modules are available to make.tcl" \n \n append h " $scriptname vendorupdate" \n append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n + append h " - update the src/vendorlib based on src/vendorlib/config.toml (todo)" \n \n append h " $scriptname vfscommonupdate" \n append h " - update the src/vfs/_vfscommon.vfs from compiled src/modules and src/lib etc" \n append h " - before calling this (followed by make project) - you can test using '(.exe) dev'" \n @@ -1213,6 +1220,8 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} } set sourcefolder $projectroot/src +set binfolder $projectroot/bin + if {$::punkboot::command eq "check"} { set sep [string repeat - 75] puts stdout $sep @@ -1348,8 +1357,8 @@ if {$::punkboot::command eq "info"} { puts stdout "- -- --- --- --- --- --- --- --- --- -- -" puts stdout "- projectroot : $projectroot" set sourcefolder $projectroot/src - set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] - set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] + set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] + set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] puts stdout "- vendorlib folders: ([llength $vendorlibfolders])" foreach fld $vendorlibfolders { puts stdout " src/$fld" @@ -1358,13 +1367,18 @@ if {$::punkboot::command eq "info"} { foreach fld $vendormodulefolders { puts stdout " src/$fld" } - set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] - puts stdout "- source module paths: [llength $source_module_folderlist]" - foreach fld $source_module_folderlist { + #set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] ;#returns only those containing .tm files + #foreach fld $source_module_folderlist { + # set relpath [punkcheck::lib::path_relative $projectroot $fld] + # puts stdout " $relpath" + #} + set projectmodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails modules_tcl* modules] + puts stdout "- source module paths: [llength $projectmodulefolders]" + #JJJ + foreach fld $projectmodulefolders { puts stdout " $fld" } - set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] - lappend projectlibfolders lib + set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl* lib] puts stdout "- source libary paths: [llength $projectlibfolders]" foreach fld $projectlibfolders { puts stdout " src/$fld" @@ -1759,7 +1773,7 @@ if {$::punkboot::command eq "bootsupport"} { -if {$::punkboot::command ni {project modules vfs}} { +if {$::punkboot::command ni {project modules libs packages vfs bin}} { puts stderr "Command $::punkboot::command not implemented - aborting." flush stderr after 100 @@ -1772,7 +1786,7 @@ if {$::punkboot::command ni {project modules vfs}} { #install src vendor contents (from version controlled src folder) to base of project (same target folders as our own src/modules etc ie to paths that go on the auto_path and in tcl::tm::list) -if {$::punkboot::command in {project modules}} { +if {$::punkboot::command in {project packages modules}} { set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] foreach vf $vendormodulefolders { lassign [split $vf _] _vm tclx @@ -1797,7 +1811,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendormodulefolders]} { puts stderr "VENDORMODULES: No src/vendormodules or src/vendormodules_tcl* folders found." } +} +if {$::punkboot::command in {project packages libs}} { set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] foreach lf $vendorlibfolders { lassign [split $lf _] _vm tclx @@ -1827,8 +1843,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendorlibfolders]} { puts stderr "VENDORLIB: No src/vendorlib or src/vendorlib_tcl* folder found." } +} - +if {$::punkboot::command in {project packages modules libs}} { ######################################################## #templates #e.g The default project layout is mainly folder structure and readme files - but has some scripts developed under the main src that we want to sync @@ -1896,6 +1913,9 @@ if {$::punkboot::command in {project modules}} { $tpl_installer destroy } } +} + +if {$::punkboot::command in {project packages libs}} { ######################################################## set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] lappend projectlibfolders lib @@ -1927,7 +1947,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $projectlibfolders]} { puts stderr "PROJECTLIB: No src/lib or src/lib_tcl* folder found." } +} +if {$::punkboot::command in {project packages modules}} { #consolidated /modules /modules_tclX folder used for target where X is tcl major version #the make process will process for any _tclX not just the major version of the current interpreter @@ -1964,9 +1986,10 @@ if {$::punkboot::command in {project modules}} { ] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } +} +if {$::punkboot::command in {project packages modules libs}} { set installername "make.tcl" - # ---------------------------------------- if {[punk::repo::is_fossil_root $projectroot]} { set config [dict create\ @@ -2013,7 +2036,7 @@ if {$::punkboot::command in {project modules}} { #review set installername "make.tcl" -if {$::punkboot::command ni {project vfs}} { +if {$::punkboot::command ni {project vfs bin}} { #command = modules puts stdout "vfs folders not checked" puts stdout " - use 'make.tcl vfscommonupdate' to copy built modules into base vfs folder" @@ -2033,6 +2056,17 @@ if {$buildfolder ne "$sourcefolder/_build"} { exit 2 } +if {$::punkboot::command eq "bin"} { + puts stdout "checking $sourcefolder/bin" + set resultdict [punkcheck::install $sourcefolder/bin $binfolder\ + -overwrite synced-targets\ + -installer "punkboot-bin"\ + -progresschannel stdout\ + ] + + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + flush stdout +} #find runtimes set rtfolder $sourcefolder/runtime @@ -2056,11 +2090,32 @@ if {![llength $runtimes]} { } set has_sdx 1 -if {[catch {exec sdx help} errM]} { - puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" - puts stderr "err: $errM" - #exit 1 - set has_sdx 0 +set sdxpath [auto_execok $binfolder/sdx] +if {$sdxpath eq ""} { + set sdxpath [auto_execok [file dirname [info nameofexecutable]]/sdx] + if {$sdxpath eq ""} { + #last resort - look on path + set sdxpath [auto_execok sdx] + } + if {$sdxpath eq ""} { + #last resort - a tclkit and sdx.kit fine + if {[file exists $binfolder/sdx.kit]} { + set tclkitpath [auto_execok $binfolder/tclkit] + if {$tclkitpath eq ""} { + set tclkitpath [auto_execok tclkit] + } + set sdxpath [list {*}$tclkitpath $binfolder/sdx.kit] + } + } + + if {$sdxpath eq "" || [catch {exec {*}$sdxpath help} errM]} { + puts stderr "FAILED to find usable sdx command or tclkit executable with sdx.bat" + puts stderr "If tclkit-based runtimes are required - check that sdx executable is in bin folder of project or in same folder as tcl/punk executable or on path" + puts stderr "This is not a problem if tcl8.7/tcl9+ kits using the preferred method 'zipfs' are to be used, or if cookfs based kits are to be used." + puts stderr "err: $errM" + #exit 1 + set has_sdx 0 + } } # -- --- --- --- --- --- --- --- --- --- @@ -2825,17 +2880,17 @@ foreach vfstail $vfs_tails { if {[catch { if {$rtname ne "-"} { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose } else { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" } else { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } - puts stderr "sdx wrap $targetkit failed" + puts stderr "$::sdxpath wrap $targetkit failed" lappend failed_kits [list kit $targetkit reason $sdxmsg] $vfs_event targetset_end FAILED $vfs_event destroy @@ -3022,6 +3077,7 @@ foreach vfstail $vfs_tails { } ;#end foreach rtname in runtimes # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- } + cd $startdir if {[llength $installed_kits]} { puts stdout "INSTALLED KITS: ([llength $installed_kits])" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index a4f56010..1b15d45a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -890,10 +890,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - puts stderr "--> pkg epoch $prev_e -> $current_e" - puts stderr "args: $args" - puts stderr "last_auto: $last_auto_path" - puts stderr "auto_path: $auto_path" + #puts stderr "--> pkg epoch $prev_e -> $current_e" + #puts stderr "args: $args" + #puts stderr "last_auto: $last_auto_path" + #puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 8ef36e27..06b145de 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -20,7 +20,7 @@ #[manpage_begin punkshell_module_scriptwrap 0 0.1.0] #[copyright "2024"] #[titledesc {scriptwrap polyglot tool}] [comment {-- Name section and table of contents description --}] -#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] +#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] #[require punk::mix::commandset::scriptwrap] #[keywords module commandset launcher scriptwrap] #[description] @@ -30,7 +30,7 @@ #*** !doctools #[section Overview] -#[para] overview of scriptwrap +#[para] overview of scriptwrap #[subsection Concepts] #[para] - @@ -74,7 +74,7 @@ package require punk::fileline namespace eval punk::mix::commandset::scriptwrap { #*** !doctools #[subsection {Namespace punk::mix::commandset::scriptwrap}] - #[para] Core API functions for punk::mix::commandset::scriptwrap + #[para] Core API functions for punk::mix::commandset::scriptwrap #[list_begin definitions] namespace export * @@ -93,7 +93,7 @@ namespace eval punk::mix::commandset::scriptwrap { foreach k [lreverse [dict keys $tdict_low_to_high]] { dict set tdict $k [dict get $tdict_low_to_high $k] } - + #set pathinfolist [dict values $tdict] set names [dict keys $tdict] @@ -142,9 +142,9 @@ namespace eval punk::mix::commandset::scriptwrap { put stderr "commandset::scriptwrap::templates_dict WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } return - } - - + } + + #A batch file with unix line-endings is sensitive to label positioning. #batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it. #see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings) @@ -808,176 +808,317 @@ namespace eval punk::mix::commandset::scriptwrap { return $result } #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site - #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf - proc multishell {filepath_or_scriptset args} { - set opts [dict create\ - -askme 1\ - -outputfolder "\uFFFF"\ - -template "\uFFFF"\ - -returnextra 0\ - -force 0\ - ] - #set known_opts [dict keys $defaults] - foreach {k v} $args { - switch -- $k { - -askme - -outputfolder - -template - -returnextra - -force { - dict set opts $k $v - } - default { - error "punk::mix::commandset::multishell error. Unrecognized option '$k'. Known-options: [dict keys $opts]" - } + #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf + #set usage "" + #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n + #append usage "The scriptset name will be used to search for .sh|.tcl|.ps1 or names as you specify in yourname.wrapconfig if it exists" \n + #append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n + #if {![string length $filepath_or_scriptset]} { + # puts stderr "No filepath_or_scriptset specified" + # puts stderr $usage + # return false + #} + proc _read_scriptset_wrap_tomlfile {fname} { + set resultd [dict create] + package require tomlish + set tomldata [readFile $fname] + #todo - fix tomlish to provide line number in ERROR structure during from_toml call. + if {[catch {tomlish::to_dict [tomlish::from_toml $tomldata]} tomldict]} { + puts stderr "Failed to parse $fname" + puts stderr "error: $tomldict" + } + if {[tomlish::dict::path::exists $tomldict {.application.template}]} { + dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] + } + set scripts [list] + if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { + set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] + foreach tvdict $arrvalues { + lappend scripts [dict get $tvdict value] } } + dict set resultd scripts $scripts - set usage "" - append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n - append usage "The scriptset name will be used to search for yourname.sh|tcl|ps1 or names as you specify in yourname.wrapconfig if it exists" \n - append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n - if {![string length $filepath_or_scriptset]} { - puts stderr "No filepath_or_scriptset specified" - puts stderr $usage - return false + set ftail [file rootname [file tail $fname]] ;#e.g example_wrap.toml + set scriptset [lindex [split $ftail _] 0] + set fallback_outputfile $scriptset.cmd + set fallback_nextshellpath "/usr/bin/env tclsh" + set fallback_nextshelltype "tcl" + + if {[tomlish::dict::path::exists $tomldict {.application.default_outputfile.value}]} { + dict set resultd default_outputfile [tomlish::dict::path::get $tomldict {.application.default_outputfile.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshellpath.value}]} { + dict set resultd default_nextshellpath [tomlish::dict::path::get $tomldict {.application.default_nextshellpath.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshelltype.value}]} { + dict set resultd default_nextshelltype [tomlish::dict::path::get $tomldict {.application.default_nextshelltype.value}] + } + foreach platform {win32 dragonflybsd freebsd netbsd linux macosx other} { + set d [dict create] + foreach field {outputfile nextshellpath nextshelltype} { + if {[tomlish::dict::path::exists $tomldict ".application.$platform.$field.value"]} { + dict set d $field [tomlish::dict::path::get $tomldict ".application.$platform.$field.value"] + } else { + if {[dict exists $resultd default_$field]} { + dict set d $field [dict get $resultd default_$field] + } else { + dict set d $field [set fallback_$field] + } + } + } + dict set resultd $platform $d } + + return $resultd + } + punk::args::define { + @id -id ::punk::mix::commandset::scriptwrap::multishell + @cmd -name punk::mix::commandset::scriptwrap::multishell\ + -summary\ + "Wrap script(s) into a polyglot cross-platform executable script."\ + -help\ + "Create a polyglot executable script that wraps constituent scripts written in + various scripting languages such as perl, tcl, shell script, powershell. + The resulting polyglot file should run cross platform on windows and various + types of unix-like OS. For use on windows the output file should be named with + a .bat or .cmd extension - but the same file with extension removed should also + be capable of running on FreeBSD, Linux etc. + Note that a polyglot script such as this may be somewhat brittle over the long + term with regards to default shells and scripting languages across platforms." + @leaders -min 1 -max 1 + filepath_or_scriptset -type string -minsize 1 -help\ + "Supply the path to a single script file to wrap, or the name of a scriptset. + The scriptset name will be used to search for .sh|.bash|.tcl|.ps1|.pl + or alternatively, names as specified in a configuration file named _wrap.toml + if it exists in the current folder, or is specified with a full path name. + If no template name/path is specified in a _wrap.toml file and no + -template argument is supplied the default punk.multishell.cmd will be used. + If the template is specified explicitly in -template as well as in the .toml + file - the supplied -template argument will override that specified in the + .toml file." + @opts + -template -type string -default "punk.multishell.cmd" -help\ + "Templates are provided from modules or paths in the current project, + so available templates will vary based on whether the multishell + command is being run from within a project directory or not. + To see available templates use punk::mix::commandset::scriptwrap::templates." + -outputfolder -type directory -default "" -help\ + "Folder to which to write resulting polyglot script. + If empty, the output will go to the /bin folder or + to the current working directory if there is no projectroot." + -askme -type boolean -default 1 -help\ + "Prompt user at console (stdin) for confirmation of operations such as + overwrite." + -force -type boolean -default 0 + -returnextra -type boolean -default 0 + @values -minvalues 0 -maxvalues 0 + } + #: + #@SET "nextshellpath[win32___________]=tclsh___________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[other___________]=tcl_____________" + #: + proc multishell {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] + lassign [dict values $argd] leaders opts values received + # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_askme [dict get $opts -askme] - set opt_template [dict get $opts -template] - set opt_outputfolder [dict get $opts -outputfolder] - set opt_returnextra [dict get $opts -returnextra] - set opt_force [dict get $opts -force] + set filepath_or_scriptset [dict get $leaders filepath_or_scriptset] + set opt_askme [dict get $opts -askme] + set opt_template [dict get $opts -template] ;#use dict exists $received -template to see if overridable in .toml + set opt_outputfolder [dict get $opts -outputfolder] + set opt_returnextra [dict get $opts -returnextra] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- - set ext [file extension $filepath_or_scriptset] set startdir [pwd] + set allowed_extensions [list tcl ps1 sh bash pl] + #TODO - distinct sections for sh vs bash? needs experiments.. + #for now we use shell-pre-launch-subprocess etc + #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] + set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] + + if {[file pathtype $filepath_or_scriptset] ni {absolute relative}} { + error "bad pathtype for '$filepath_or_scriptset' (expected absolute or relative path, or name of scriptset)" + } - - - #first check if relative or absolute path matches a file + #first check if absolute path matches a file or relative path from cwd matches a file if {[file pathtype $filepath_or_scriptset] eq "absolute"} { - set specified_path $filepath_or_scriptset + set specified_path $filepath_or_scriptset } else { set specified_path [file join $startdir $filepath_or_scriptset] } + set scriptdir [file dirname $specified_path] + set ext [string trim [file extension $filepath_or_scriptset] .] - set allowed_extensions [list wrapconfig tcl ps1 sh bash pl] - set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] - #set allowed_extensions [list tcl] - set found_script 0 - if {[file exists $specified_path]} { - set found_script 1 + set scriptset "" + if {$ext eq ""} { + set scriptset [file rootname [file tail $specified_path]] + } elseif {$ext eq "toml"} { + set tomltail [file tail $specified_path] + if {[string match *_wrap.toml $tomltail]} { + set scriptset [lindex [split $tomltail _] 0] + #if .toml was specified - the config file must exist + if {![file exists $specified_path]} { + if {[file pathtype $filepath_or_scriptset] eq "relative"} { + puts stderr "unable to locate '$specified_path' - will continue search in src/scriptapps folder" + } else { + #caller was specific about path - no fallback to src/scriptapps + error "unable to locate '$specified_path'" + } + } + } else { + error "supplied toml file must be of form _wrap.toml" + } } else { - foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { - if {[file exists $filepath_or_scriptset.$e]} { - set found_script 1 - break + if {$ext ni $allowed_extensions} { + error "supplied filepath_or_scriptset must be the name of a scriptset without extension, a file named _wrap.toml, or a script with one of the extensions: $allowed_extensions" + } + } + + set list_input_files [list] + set configd [dict create] + if {$scriptset ne ""} { + puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" + #.toml file may or may not exist + if {[file exists ${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptdir $s] + } + } + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" + return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptdir" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptdir/$scriptset.$e]} { + lappend list_input_files $scriptdir/$scriptset.$e + } } } + } else { + #expect a single script + if {[file exists $specified_path]} { + lappend list_input_files $specified_path + } } + set found_script [expr {[llength $list_input_files] > 0}] #TODO! - use get_wrapper_folders - multishell should use same available templates as the 'templates' function - set scriptset [file rootname [file tail $specified_path]] if {$found_script} { - if {[file type $specified_path] eq "file"} { - set specified_root [file dirname $specified_path] - set pathinfo [punk::repo::find_repos [file dirname $specified_path]] - set projectroot [dict get $pathinfo closest] + #found scripts at absolute path - or path relative to cwd + set scriptroot $scriptdir + set pathinfo [punk::repo::find_repos $scriptroot] + set projectroot [dict get $pathinfo closest] + if {[file exists $scriptroot/wrappers]} { + set customwrapper_folder $scriptroot/wrappers + } else { + #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder if {[string length $projectroot]} { - #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - set customwrapper_folder $projectroot/src/scriptapps/wrappers - } + set customwrapper_folder $projectroot/src/scriptapps/wrappers } else { #outside of any project - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - #no customwrapper folder available - set customwrapper_folder "" - } + set customwrapper_folder "" } - } else { - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false } } else { + if {[file pathtype $filepath_or_scriptset] eq "absolute"} { + return false + } set pathinfo [punk::repo::find_repos $startdir] set projectroot [dict get $pathinfo closest] - if {[string length $projectroot]} { - if {[llength [file split $filepath_or_scriptset]] > 1} { - puts stderr "filepath_or_scriptset looks like a path - but doesn't seem to point to a file" - puts stderr "Ensure you are within a project and use just the name of the scriptset, or pass in the full correct path or relative path to current directory" - puts stderr $usage - return false - } else { - #we've already ruled out empty string - so must have a single element representing scriptset - possibly with file extension - set scriptroot $projectroot/src/scriptapps - set customwrapper_folder $projectroot/src/scriptapps/wrappers - #check something matches the scriptset.. - set something_found "" - if {[file exists $scriptroot/$scriptset]} { - set found_script 1 - set something_found $scriptroot/$scriptset ;#extensionless file - that's ok too - } else { - foreach e $allowed_extensions { - if {[file exists $scriptroot/$scriptset.$e]} { - set found_script 1 - set something_found $scriptroot/$scriptset.$e - break - } + if {![string length $projectroot]} { + puts stderr "No matching scripts or config found for $filepath_or_scriptset, and you are not within a directory where projectroot and src/scriptapps can be determined" + return false + } + + set scriptroot $projectroot/src/scriptapps + set customwrapper_folder $projectroot/src/scriptapps/wrappers + #check something matches the scriptset.. + if {$scriptset ne ""} { + #.toml file may or may not exist + if {[file exists $scriptroot/${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptroot $s] } } - if {!$found_script} { - puts stderr "Searched within $scriptroot" - puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" - puts stderr $usage + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" return false - } else { - if {[file type $something_found] ne "file"} { - puts stderr "Found '$something_found'" - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptroot" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptroot/$scriptset.$e]} { + lappend list_input_files $scriptroot/$scriptset.$e } } } - } else { - puts stderr "filepath_or_scriptset parameter doesn't seem to refer to a file, and you are not within a directory where projectroot and src/scriptapps/wrappers can be determined" - puts stderr $usage - return false + #expect a single script + if {[file exists $scriptroot/$filepath_or_scriptset]} { + if {[file type $scriptroot/$filepath_or_scriptset] ne "file"} { + puts stderr "wrap_in_multishell doesn't currently support a directory as the path. path: $scriptroot/$filepath_or_scriptset" + return false + } + lappend list_input_files $scriptroot/$filepath_or_scriptset + } } - } - #assertion - customwrapper_folder var exists - but might be empty + set found_script [expr {[llength $list_input_files] > 0}] - - if {[string length $ext]} { - #If there was an explicitly supplied extension - then that file should exist - if {![file exists $scriptroot/$scriptset.$ext]} { - puts stderr "Explicit extension .$ext was supplied - but matching file not found." - puts stderr $usage - return false - } else { - if {$ext eq "wrapconfig"} { - set process_extensions ALLFOUNDORCONFIGURED + #---------------------- + if {!$found_script} { + puts stderr "Searched within $scriptdir and $scriptroot" + if {$scriptset ne ""} { + puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" } else { - set process_extensions $ext + puts stderr "Unable to find file $filepath_or_scriptset" } + return false } - } else { - #no explicit extension - process all for scriptset - set process_extensions ALLFOUNDORCONFIGURED + } - #process_extensions - either a single one - or all found or as per .wrapconfig + #assertion - customwrapper_folder var exists - but might be empty - if {$opt_template eq "\uFFFF"} { - set templatename punk.multishell.cmd + if {[dict exists $configd template]} { + set templatename [dict get $configd template] } else { - set templatename $opt_template + if {$opt_template eq "\uFFFF"} { + set templatename punk.multishell.cmd + } else { + set templatename $opt_template + } } set templatename_root [file rootname [file tail $templatename]] @@ -995,7 +1136,7 @@ namespace eval punk::mix::commandset::scriptwrap { set template_base_dict [punk::mix::base::lib::get_template_basefolders] set tpldirs [list] dict for {tdir tsourceinfo} $template_base_dict { - set vendor [dict get $tsourceinfo vendor] + set vendor [dict get $tsourceinfo vendor] if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { lappend tpldirs $tdir } elseif {[file exists $tdir/utility/scriptappwrappers/${templatename_fileroot}[file extension $templatename]]} { @@ -1032,7 +1173,7 @@ namespace eval punk::mix::commandset::scriptwrap { } - if {$opt_outputfolder eq "\uFFFF"} { + if {$opt_outputfolder eq ""} { #outputfolder not explicitly specified by caller if {[string length $projectroot]} { set output_folder [file join $projectroot/bin] @@ -1056,13 +1197,36 @@ namespace eval punk::mix::commandset::scriptwrap { #todo - #output_file extension may also depend on the template being used.. and/or the .wrapconfig - if {$::tcl_platform(platform) eq "windows"} { - set output_extension cmd + #output_file extension may also depend on the template being used.. and/or the _wrap.toml config + + if {[dict size $configd]} { + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other + } + } + set out [dict get $configd $ptype outputfile] + set output_file [file join $output_folder $out] } else { - set output_extension sh + #no _wrap.toml file available + if {$::tcl_platform(platform) eq "windows"} { + set output_extension .cmd + } else { + set output_extension .sh + } + if {$scriptset ne ""} { + set output_file [file join $output_folder $scriptset$output_extension] + } else { + set infile [lindex $list_input_files 0] + set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] + } } - set output_file [file join $output_folder $scriptset.$output_extension] + + if {[file exists $output_file]} { set fdexisting [open $output_file r] fconfigure $fdexisting -translation binary @@ -1103,13 +1267,10 @@ namespace eval punk::mix::commandset::scriptwrap { #foreach ln $template_lines { #} - set list_input_files [list] - if {$process_extensions eq "ALLFOUNDORCONFIGURED"} { - #todo - look for .wrapconfig or all extensions for the scriptset - puts stderr "Sorry - only single input file supported. Supply a file extension or use a .wrapconfig with a single input file for now - implementation incomplete" + if {[llength $list_input_files] > 1} { + #todo + puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" return false - } else { - lappend list_input_files $scriptroot/$scriptset.$ext } #todo - split template at each etc marker and build a dict of parts @@ -1117,7 +1278,6 @@ namespace eval punk::mix::commandset::scriptwrap { #hack - process one input set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] fconfigure $fdscript -translation binary set script_data [read $fdscript] @@ -1131,7 +1291,8 @@ namespace eval punk::mix::commandset::scriptwrap { } puts stdout "-----------------------------------------------\n" puts stdout "Target for above script data is '$output_file'" - set lang [dict get $extension_langs [string tolower $ext]] + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] puts stdout "Language of script being wrapped is $lang" if {$opt_askme} { set answer [util::askuser "Does this look correct? Y|N"] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 6bd826e2..f8e55b02 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -444,9 +444,8 @@ tcl::namespace::eval punk::ns { set nspath [string map {:::: ::} $nspath] set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] - if {[lindex $parts end] eq ""} { - - } + #if {[lindex $parts end] eq ""} { + #} return $parts } @@ -531,6 +530,21 @@ tcl::namespace::eval punk::ns { return [regexp [dict get $ns_re_cache $glob] $path] } + #namespace tree without globbing or weird ns consideration + proc nstree_raw {{location ::}} { + if {![string match ::* $location]} { + error "nstree_raw requires a fully qualified namespace" + } + nstree_rawlist $location + } + proc nstree_rawlist {location} { + set nslist [list $location] + foreach ch [::namespace children $location] { + lappend nslist {*}[nstree_rawlist $ch] + } + return $nslist + } + proc nstree {{location ""}} { if {![string match ::* $location]} { set nscaller [uplevel 1 {::namespace current}] @@ -3899,6 +3913,7 @@ tcl::namespace::eval punk::ns { } proc _pkguse_vars {varnames} { + #review - obsolete? while {"pkguse_vars_[incr n]" in $varnames} {} #return [concat $varnames pkguse_vars_$n] return [list {*}$varnames pkguse_vars_$n] @@ -3932,10 +3947,12 @@ tcl::namespace::eval punk::ns { #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically + variable pkguse_package_to_namespace [dict create] proc pkguse {args} { + variable pkguse_package_to_namespace set argd [punk::args::parse $args withid ::punk::ns::pkguse] lassign [dict values $argd] leaders opts values received - puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + #puts stderr "leaders:$leaders opts:$opts values:$values received:$received" set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] if {[dict exists $received script]} { @@ -3967,68 +3984,159 @@ tcl::namespace::eval punk::ns { set ver "";# tcl version? } default { - if {[string match ::* $pkg_or_existing_ns]} { - set pkg_unqualified [string range $pkg_or_existing_ns 2 end] - if {![tcl::namespace::exists $pkg_or_existing_ns]} { - set ver [package require $pkg_unqualified] - } else { - set ver "" - } + #- comparing namespaces_before vs namespaces_after only works if the package was not previously loaded + #we could either go to the somewhat expensive route of steaming up an interp with the same auto_path & tcl::tm::list each time.. + #or cache the result of the namespace we picked for later pkguse calls (pkguse_package_to_namespace dict) + #we are using the cache method - but this also doesn't help for packages previously loaded by normal package require + #our aim is for pkguse to be deterministic in what namespace it finds - even if it doesn't always get the ideal one (e.g cookiejar, see below) + #To determine appropriate namespace for already loaded packages where we have no cache entry - we may still need the helper interp mechanism + #The helper interp could be persistent - but only so long as the auto_path/tcl::tm::list values are in sync + #review. + + #also see img::png img::raw etc + #these don't directly load namespaces or direct commands.. just change behaviour of existing commands? + #but they can load things like tk (ttk namespace) first one creates ::tkimg? + + if {[string match ::* $pkg_or_existing_ns] && [tcl::namespace::exists $pkg_or_existing_ns]} { + #pkguse on an existing full qualified namespace does no package require set ns $pkg_or_existing_ns + set ver "" } else { - set pkg_unqualified $pkg_or_existing_ns - set ver [package require $pkg_unqualified] - set ns ::$pkg_unqualified - } - #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index - set previous_command_count 0 - if {[namespace exists $ns]} { - set previous_command_count [llength [info commands ${ns}::*]] - } + if {[string match ::* $pkg_or_existing_ns]} { + set pkg_unqualified [string range $pkg_or_existing_ns 2 end] + } else { + set pkg_unqualified $pkg_or_existing_ns + } + #foreach equiv of while 1 - just to allow early exit with break + foreach code_block single { + if {[dict exists $pkguse_package_to_namespace $pkg_unqualified]} { + set ns [dict get $pkguse_package_to_namespace $pkg_unqualified] + set ver [package provide $pkg_unqualified] + break + } + if {[package provide $pkg_unqualified] ne ""} { + #package has already been loaded + if {[namespace exists ::$pkg_unqualified]} { + set ns ::$pkg_unqualified + set ver [package provide $pkg_unqualified] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + break + } + #existing package but no matching namespace.. + #- load in throwaway interp and see what cmds/namespaces created + interp create nstest + try { + nstest eval {tcl::tm::remove {*}[tcl::tm::list]} + nstest eval [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] + nstest eval [list set ::auto_path $::auto_path] + nstest eval {package require punk::ns} + set ns "" + if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] + set ns [nstest eval $script] + } else { + puts "couldn't test pkg $pkg_unqualified\n$errMsg" + } + } finally { + interp delete nstest + } - #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands - #for the purposes of pkguse - which most commonly interactive - we want the namespace populated - #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + set ver [package provide $pkg_unqualified] + break + } - if {!$ns_populated} { - #we will catch-run an auto_index entry if any - #auto_index entry may or may not be prefixed with :: - set keys [list] - #first look for exact pkg_unqualified and ::pkg_unqualified - #leave these at beginning of keys list - if {[array exists ::auto_index($pkg_unqualified)]} { - lappend keys $pkg_unqualified - } - if {[array exists ::auto_index(::$pkg_unqualified)]} { - lappend keys ::$pkg_unqualified - } - #as auto_index is an array - we could get keys in arbitrary order - set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] - lappend keys {*}$matches - set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] - lappend keys {*}$matches - set ns_populated 0 - set i 0 - set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing - set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] - while {!$ns_populated && $i < [llength $keys]} { - #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base - #e.g if we are loading ::x::y - #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc - set k [lindex $keys $i] - set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] - if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { - set auto_source [set ::auto_index($k)] - if {$auto_source ni $already_sourced} { - uplevel 1 $auto_source - lappend already_sourced $auto_source - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + #pkg not loaded + set namespaces_before [nstree_rawlist ::] ;#approx 1ms for 500 or so namespaces - not cheap but bearable + #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index + #gathering prior cmdcount for every ns in system is also a somewhat expensive operation.. review + #we don't know for sure that the namespace for the package require operation actually matches the package name + #e.g tcllib inifile package uses namespace ::ini + #e.g sqlite3 package adds commands to the global namespace + set dict_ns_commandcounts [dict create] + foreach nsb $namespaces_before { + dict set dict_ns_commandcounts $nsb [llength [info commands ${nsb}::*]] + } + + set ver [package require $pkg_unqualified] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + set namespaces_after [nstree_rawlist ::] + + if {[llength $namespaces_after] > [llength $namespaces_before]} { + set namespaces_new [struct::set difference $namespaces_after $namespaces_before] + if {$ns ni $namespaces_new} { + #todo - use shortest result? what if this is a namespace from a required sub package? + #e.g cookiejar loads sqlite3,http,tcl::idna which creates ::sqlite3 etc - but cookiejar just creates an object at ::http::cookiejar + #In this specific case we end up in oo::ObjXXX - but would be better placed in ::http, where the new cookiejar command resides + #review - todo? + set pkgs [package names] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + #find something new - that doesn't match another package name + foreach new $namespaces_new { + if {[lsearch $pkgs [string trimleft $new :]] == -1} { + set ns $new + break + } + } } } - incr i - } + if {[tcl::namespace::exists $ns]} { + #review - only cache if exists? + dict set pkguse_package_to_namespace $pkg_unqualified $ns; + } + set previous_command_count 0 + if {[dict exists $dict_ns_commandcounts $ns]} { + set previous_command_count [dict get $dict_ns_commandcounts $ns] + } + + #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands + #for the purposes of pkguse - which most commonly interactive - we want the namespace populated + #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + + if {!$ns_populated} { + #we will catch-run an auto_index entry if any + #auto_index entry may or may not be prefixed with :: + set keys [list] + #first look for exact pkg_unqualified and ::pkg_unqualified + #leave these at beginning of keys list + if {[array exists ::auto_index($pkg_unqualified)]} { + lappend keys $pkg_unqualified + } + if {[array exists ::auto_index(::$pkg_unqualified)]} { + lappend keys ::$pkg_unqualified + } + #as auto_index is an array - we could get keys in arbitrary order + set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] + lappend keys {*}$matches + set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] + lappend keys {*}$matches + set ns_populated 0 + set i 0 + set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing + set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] + while {!$ns_populated && $i < [llength $keys]} { + #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base + #e.g if we are loading ::x::y + #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc + set k [lindex $keys $i] + set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] + if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { + set auto_source [set ::auto_index($k)] + if {$auto_source ni $already_sourced} { + puts stderr "pkguse sourcing auto_index script $auto_source" + uplevel 1 $auto_source + lappend already_sourced $auto_source + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + } + } + incr i + } + + } + + }; # end foreach code_block single - scope for use of 'break' } } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index a31e255e..fd84ec8d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -3567,7 +3567,6 @@ namespace eval repl { if {[catch { package require punk::args - catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical package require punk::config package require punk::ns #puts stderr "loading natsort" @@ -3589,6 +3588,7 @@ namespace eval repl { }} [punk::config::configure running] package require textblock + catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm index 472edc54..f2f4a3af 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -6007,7 +6007,7 @@ tcl::namespace::eval textblock { proc welcome_test {} { package require punk::ansi - set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] + set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/ROY-WELC.ANS 80x8]] # Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index 2de13afb..37f36a9a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl @@ -22,7 +22,7 @@ namespace eval ::punkboot { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /? -h] - variable known_commands [list project modules vfs info check shell vendorupdate bootsupport vfscommonupdate ] + variable known_commands [list project modules libs packages vfs bin info check shell vendorupdate bootsupport vfscommonupdate ] } @@ -1077,10 +1077,16 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - This help." \n \n append h " $scriptname project ?-k?" \n append h " - this is the literal word project - and confirms you want to run the project build - which includes src/vfs/* checks and builds" \n - append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n - append h " - built modules go into /modules /lib etc." \n \n + append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n + append h " - builds/copies .tm modules from src to /modules etc and pkgIndex.tcl based libraries from src to /lib etc." \n \n append h " $scriptname modules" \n - append h " - build modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " - build (or copy if build not required) .tm modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname libs" \n + append h " - build (or copy if build not required) pkgIndex.tcl based libraries from src/lib src/vendorlib etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname packages" \n + append h " - build (or copy if build not required) both .tm and pkgIndex.tcl based packages from src to their corresponding locations under " \n append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n @@ -1089,6 +1095,7 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - bootsupport modules are available to make.tcl" \n \n append h " $scriptname vendorupdate" \n append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n + append h " - update the src/vendorlib based on src/vendorlib/config.toml (todo)" \n \n append h " $scriptname vfscommonupdate" \n append h " - update the src/vfs/_vfscommon.vfs from compiled src/modules and src/lib etc" \n append h " - before calling this (followed by make project) - you can test using '(.exe) dev'" \n @@ -1213,6 +1220,8 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} } set sourcefolder $projectroot/src +set binfolder $projectroot/bin + if {$::punkboot::command eq "check"} { set sep [string repeat - 75] puts stdout $sep @@ -1348,8 +1357,8 @@ if {$::punkboot::command eq "info"} { puts stdout "- -- --- --- --- --- --- --- --- --- -- -" puts stdout "- projectroot : $projectroot" set sourcefolder $projectroot/src - set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] - set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] + set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] + set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] puts stdout "- vendorlib folders: ([llength $vendorlibfolders])" foreach fld $vendorlibfolders { puts stdout " src/$fld" @@ -1358,13 +1367,18 @@ if {$::punkboot::command eq "info"} { foreach fld $vendormodulefolders { puts stdout " src/$fld" } - set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] - puts stdout "- source module paths: [llength $source_module_folderlist]" - foreach fld $source_module_folderlist { + #set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] ;#returns only those containing .tm files + #foreach fld $source_module_folderlist { + # set relpath [punkcheck::lib::path_relative $projectroot $fld] + # puts stdout " $relpath" + #} + set projectmodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails modules_tcl* modules] + puts stdout "- source module paths: [llength $projectmodulefolders]" + #JJJ + foreach fld $projectmodulefolders { puts stdout " $fld" } - set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] - lappend projectlibfolders lib + set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl* lib] puts stdout "- source libary paths: [llength $projectlibfolders]" foreach fld $projectlibfolders { puts stdout " src/$fld" @@ -1759,7 +1773,7 @@ if {$::punkboot::command eq "bootsupport"} { -if {$::punkboot::command ni {project modules vfs}} { +if {$::punkboot::command ni {project modules libs packages vfs bin}} { puts stderr "Command $::punkboot::command not implemented - aborting." flush stderr after 100 @@ -1772,7 +1786,7 @@ if {$::punkboot::command ni {project modules vfs}} { #install src vendor contents (from version controlled src folder) to base of project (same target folders as our own src/modules etc ie to paths that go on the auto_path and in tcl::tm::list) -if {$::punkboot::command in {project modules}} { +if {$::punkboot::command in {project packages modules}} { set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] foreach vf $vendormodulefolders { lassign [split $vf _] _vm tclx @@ -1797,7 +1811,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendormodulefolders]} { puts stderr "VENDORMODULES: No src/vendormodules or src/vendormodules_tcl* folders found." } +} +if {$::punkboot::command in {project packages libs}} { set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] foreach lf $vendorlibfolders { lassign [split $lf _] _vm tclx @@ -1827,8 +1843,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendorlibfolders]} { puts stderr "VENDORLIB: No src/vendorlib or src/vendorlib_tcl* folder found." } +} - +if {$::punkboot::command in {project packages modules libs}} { ######################################################## #templates #e.g The default project layout is mainly folder structure and readme files - but has some scripts developed under the main src that we want to sync @@ -1896,6 +1913,9 @@ if {$::punkboot::command in {project modules}} { $tpl_installer destroy } } +} + +if {$::punkboot::command in {project packages libs}} { ######################################################## set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] lappend projectlibfolders lib @@ -1927,7 +1947,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $projectlibfolders]} { puts stderr "PROJECTLIB: No src/lib or src/lib_tcl* folder found." } +} +if {$::punkboot::command in {project packages modules}} { #consolidated /modules /modules_tclX folder used for target where X is tcl major version #the make process will process for any _tclX not just the major version of the current interpreter @@ -1964,9 +1986,10 @@ if {$::punkboot::command in {project modules}} { ] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } +} +if {$::punkboot::command in {project packages modules libs}} { set installername "make.tcl" - # ---------------------------------------- if {[punk::repo::is_fossil_root $projectroot]} { set config [dict create\ @@ -2013,7 +2036,7 @@ if {$::punkboot::command in {project modules}} { #review set installername "make.tcl" -if {$::punkboot::command ni {project vfs}} { +if {$::punkboot::command ni {project vfs bin}} { #command = modules puts stdout "vfs folders not checked" puts stdout " - use 'make.tcl vfscommonupdate' to copy built modules into base vfs folder" @@ -2033,6 +2056,17 @@ if {$buildfolder ne "$sourcefolder/_build"} { exit 2 } +if {$::punkboot::command eq "bin"} { + puts stdout "checking $sourcefolder/bin" + set resultdict [punkcheck::install $sourcefolder/bin $binfolder\ + -overwrite synced-targets\ + -installer "punkboot-bin"\ + -progresschannel stdout\ + ] + + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + flush stdout +} #find runtimes set rtfolder $sourcefolder/runtime @@ -2056,11 +2090,32 @@ if {![llength $runtimes]} { } set has_sdx 1 -if {[catch {exec sdx help} errM]} { - puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" - puts stderr "err: $errM" - #exit 1 - set has_sdx 0 +set sdxpath [auto_execok $binfolder/sdx] +if {$sdxpath eq ""} { + set sdxpath [auto_execok [file dirname [info nameofexecutable]]/sdx] + if {$sdxpath eq ""} { + #last resort - look on path + set sdxpath [auto_execok sdx] + } + if {$sdxpath eq ""} { + #last resort - a tclkit and sdx.kit fine + if {[file exists $binfolder/sdx.kit]} { + set tclkitpath [auto_execok $binfolder/tclkit] + if {$tclkitpath eq ""} { + set tclkitpath [auto_execok tclkit] + } + set sdxpath [list {*}$tclkitpath $binfolder/sdx.kit] + } + } + + if {$sdxpath eq "" || [catch {exec {*}$sdxpath help} errM]} { + puts stderr "FAILED to find usable sdx command or tclkit executable with sdx.bat" + puts stderr "If tclkit-based runtimes are required - check that sdx executable is in bin folder of project or in same folder as tcl/punk executable or on path" + puts stderr "This is not a problem if tcl8.7/tcl9+ kits using the preferred method 'zipfs' are to be used, or if cookfs based kits are to be used." + puts stderr "err: $errM" + #exit 1 + set has_sdx 0 + } } # -- --- --- --- --- --- --- --- --- --- @@ -2825,17 +2880,17 @@ foreach vfstail $vfs_tails { if {[catch { if {$rtname ne "-"} { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose } else { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" } else { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } - puts stderr "sdx wrap $targetkit failed" + puts stderr "$::sdxpath wrap $targetkit failed" lappend failed_kits [list kit $targetkit reason $sdxmsg] $vfs_event targetset_end FAILED $vfs_event destroy @@ -3022,6 +3077,7 @@ foreach vfstail $vfs_tails { } ;#end foreach rtname in runtimes # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- } + cd $startdir if {[llength $installed_kits]} { puts stdout "INSTALLED KITS: ([llength $installed_kits])" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index a4f56010..1b15d45a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -890,10 +890,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - puts stderr "--> pkg epoch $prev_e -> $current_e" - puts stderr "args: $args" - puts stderr "last_auto: $last_auto_path" - puts stderr "auto_path: $auto_path" + #puts stderr "--> pkg epoch $prev_e -> $current_e" + #puts stderr "args: $args" + #puts stderr "last_auto: $last_auto_path" + #puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 8ef36e27..06b145de 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -20,7 +20,7 @@ #[manpage_begin punkshell_module_scriptwrap 0 0.1.0] #[copyright "2024"] #[titledesc {scriptwrap polyglot tool}] [comment {-- Name section and table of contents description --}] -#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] +#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] #[require punk::mix::commandset::scriptwrap] #[keywords module commandset launcher scriptwrap] #[description] @@ -30,7 +30,7 @@ #*** !doctools #[section Overview] -#[para] overview of scriptwrap +#[para] overview of scriptwrap #[subsection Concepts] #[para] - @@ -74,7 +74,7 @@ package require punk::fileline namespace eval punk::mix::commandset::scriptwrap { #*** !doctools #[subsection {Namespace punk::mix::commandset::scriptwrap}] - #[para] Core API functions for punk::mix::commandset::scriptwrap + #[para] Core API functions for punk::mix::commandset::scriptwrap #[list_begin definitions] namespace export * @@ -93,7 +93,7 @@ namespace eval punk::mix::commandset::scriptwrap { foreach k [lreverse [dict keys $tdict_low_to_high]] { dict set tdict $k [dict get $tdict_low_to_high $k] } - + #set pathinfolist [dict values $tdict] set names [dict keys $tdict] @@ -142,9 +142,9 @@ namespace eval punk::mix::commandset::scriptwrap { put stderr "commandset::scriptwrap::templates_dict WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } return - } - - + } + + #A batch file with unix line-endings is sensitive to label positioning. #batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it. #see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings) @@ -808,176 +808,317 @@ namespace eval punk::mix::commandset::scriptwrap { return $result } #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site - #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf - proc multishell {filepath_or_scriptset args} { - set opts [dict create\ - -askme 1\ - -outputfolder "\uFFFF"\ - -template "\uFFFF"\ - -returnextra 0\ - -force 0\ - ] - #set known_opts [dict keys $defaults] - foreach {k v} $args { - switch -- $k { - -askme - -outputfolder - -template - -returnextra - -force { - dict set opts $k $v - } - default { - error "punk::mix::commandset::multishell error. Unrecognized option '$k'. Known-options: [dict keys $opts]" - } + #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf + #set usage "" + #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n + #append usage "The scriptset name will be used to search for .sh|.tcl|.ps1 or names as you specify in yourname.wrapconfig if it exists" \n + #append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n + #if {![string length $filepath_or_scriptset]} { + # puts stderr "No filepath_or_scriptset specified" + # puts stderr $usage + # return false + #} + proc _read_scriptset_wrap_tomlfile {fname} { + set resultd [dict create] + package require tomlish + set tomldata [readFile $fname] + #todo - fix tomlish to provide line number in ERROR structure during from_toml call. + if {[catch {tomlish::to_dict [tomlish::from_toml $tomldata]} tomldict]} { + puts stderr "Failed to parse $fname" + puts stderr "error: $tomldict" + } + if {[tomlish::dict::path::exists $tomldict {.application.template}]} { + dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] + } + set scripts [list] + if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { + set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] + foreach tvdict $arrvalues { + lappend scripts [dict get $tvdict value] } } + dict set resultd scripts $scripts - set usage "" - append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n - append usage "The scriptset name will be used to search for yourname.sh|tcl|ps1 or names as you specify in yourname.wrapconfig if it exists" \n - append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n - if {![string length $filepath_or_scriptset]} { - puts stderr "No filepath_or_scriptset specified" - puts stderr $usage - return false + set ftail [file rootname [file tail $fname]] ;#e.g example_wrap.toml + set scriptset [lindex [split $ftail _] 0] + set fallback_outputfile $scriptset.cmd + set fallback_nextshellpath "/usr/bin/env tclsh" + set fallback_nextshelltype "tcl" + + if {[tomlish::dict::path::exists $tomldict {.application.default_outputfile.value}]} { + dict set resultd default_outputfile [tomlish::dict::path::get $tomldict {.application.default_outputfile.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshellpath.value}]} { + dict set resultd default_nextshellpath [tomlish::dict::path::get $tomldict {.application.default_nextshellpath.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshelltype.value}]} { + dict set resultd default_nextshelltype [tomlish::dict::path::get $tomldict {.application.default_nextshelltype.value}] + } + foreach platform {win32 dragonflybsd freebsd netbsd linux macosx other} { + set d [dict create] + foreach field {outputfile nextshellpath nextshelltype} { + if {[tomlish::dict::path::exists $tomldict ".application.$platform.$field.value"]} { + dict set d $field [tomlish::dict::path::get $tomldict ".application.$platform.$field.value"] + } else { + if {[dict exists $resultd default_$field]} { + dict set d $field [dict get $resultd default_$field] + } else { + dict set d $field [set fallback_$field] + } + } + } + dict set resultd $platform $d } + + return $resultd + } + punk::args::define { + @id -id ::punk::mix::commandset::scriptwrap::multishell + @cmd -name punk::mix::commandset::scriptwrap::multishell\ + -summary\ + "Wrap script(s) into a polyglot cross-platform executable script."\ + -help\ + "Create a polyglot executable script that wraps constituent scripts written in + various scripting languages such as perl, tcl, shell script, powershell. + The resulting polyglot file should run cross platform on windows and various + types of unix-like OS. For use on windows the output file should be named with + a .bat or .cmd extension - but the same file with extension removed should also + be capable of running on FreeBSD, Linux etc. + Note that a polyglot script such as this may be somewhat brittle over the long + term with regards to default shells and scripting languages across platforms." + @leaders -min 1 -max 1 + filepath_or_scriptset -type string -minsize 1 -help\ + "Supply the path to a single script file to wrap, or the name of a scriptset. + The scriptset name will be used to search for .sh|.bash|.tcl|.ps1|.pl + or alternatively, names as specified in a configuration file named _wrap.toml + if it exists in the current folder, or is specified with a full path name. + If no template name/path is specified in a _wrap.toml file and no + -template argument is supplied the default punk.multishell.cmd will be used. + If the template is specified explicitly in -template as well as in the .toml + file - the supplied -template argument will override that specified in the + .toml file." + @opts + -template -type string -default "punk.multishell.cmd" -help\ + "Templates are provided from modules or paths in the current project, + so available templates will vary based on whether the multishell + command is being run from within a project directory or not. + To see available templates use punk::mix::commandset::scriptwrap::templates." + -outputfolder -type directory -default "" -help\ + "Folder to which to write resulting polyglot script. + If empty, the output will go to the /bin folder or + to the current working directory if there is no projectroot." + -askme -type boolean -default 1 -help\ + "Prompt user at console (stdin) for confirmation of operations such as + overwrite." + -force -type boolean -default 0 + -returnextra -type boolean -default 0 + @values -minvalues 0 -maxvalues 0 + } + #: + #@SET "nextshellpath[win32___________]=tclsh___________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[other___________]=tcl_____________" + #: + proc multishell {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] + lassign [dict values $argd] leaders opts values received + # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_askme [dict get $opts -askme] - set opt_template [dict get $opts -template] - set opt_outputfolder [dict get $opts -outputfolder] - set opt_returnextra [dict get $opts -returnextra] - set opt_force [dict get $opts -force] + set filepath_or_scriptset [dict get $leaders filepath_or_scriptset] + set opt_askme [dict get $opts -askme] + set opt_template [dict get $opts -template] ;#use dict exists $received -template to see if overridable in .toml + set opt_outputfolder [dict get $opts -outputfolder] + set opt_returnextra [dict get $opts -returnextra] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- - set ext [file extension $filepath_or_scriptset] set startdir [pwd] + set allowed_extensions [list tcl ps1 sh bash pl] + #TODO - distinct sections for sh vs bash? needs experiments.. + #for now we use shell-pre-launch-subprocess etc + #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] + set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] + + if {[file pathtype $filepath_or_scriptset] ni {absolute relative}} { + error "bad pathtype for '$filepath_or_scriptset' (expected absolute or relative path, or name of scriptset)" + } - - - #first check if relative or absolute path matches a file + #first check if absolute path matches a file or relative path from cwd matches a file if {[file pathtype $filepath_or_scriptset] eq "absolute"} { - set specified_path $filepath_or_scriptset + set specified_path $filepath_or_scriptset } else { set specified_path [file join $startdir $filepath_or_scriptset] } + set scriptdir [file dirname $specified_path] + set ext [string trim [file extension $filepath_or_scriptset] .] - set allowed_extensions [list wrapconfig tcl ps1 sh bash pl] - set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] - #set allowed_extensions [list tcl] - set found_script 0 - if {[file exists $specified_path]} { - set found_script 1 + set scriptset "" + if {$ext eq ""} { + set scriptset [file rootname [file tail $specified_path]] + } elseif {$ext eq "toml"} { + set tomltail [file tail $specified_path] + if {[string match *_wrap.toml $tomltail]} { + set scriptset [lindex [split $tomltail _] 0] + #if .toml was specified - the config file must exist + if {![file exists $specified_path]} { + if {[file pathtype $filepath_or_scriptset] eq "relative"} { + puts stderr "unable to locate '$specified_path' - will continue search in src/scriptapps folder" + } else { + #caller was specific about path - no fallback to src/scriptapps + error "unable to locate '$specified_path'" + } + } + } else { + error "supplied toml file must be of form _wrap.toml" + } } else { - foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { - if {[file exists $filepath_or_scriptset.$e]} { - set found_script 1 - break + if {$ext ni $allowed_extensions} { + error "supplied filepath_or_scriptset must be the name of a scriptset without extension, a file named _wrap.toml, or a script with one of the extensions: $allowed_extensions" + } + } + + set list_input_files [list] + set configd [dict create] + if {$scriptset ne ""} { + puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" + #.toml file may or may not exist + if {[file exists ${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptdir $s] + } + } + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" + return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptdir" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptdir/$scriptset.$e]} { + lappend list_input_files $scriptdir/$scriptset.$e + } } } + } else { + #expect a single script + if {[file exists $specified_path]} { + lappend list_input_files $specified_path + } } + set found_script [expr {[llength $list_input_files] > 0}] #TODO! - use get_wrapper_folders - multishell should use same available templates as the 'templates' function - set scriptset [file rootname [file tail $specified_path]] if {$found_script} { - if {[file type $specified_path] eq "file"} { - set specified_root [file dirname $specified_path] - set pathinfo [punk::repo::find_repos [file dirname $specified_path]] - set projectroot [dict get $pathinfo closest] + #found scripts at absolute path - or path relative to cwd + set scriptroot $scriptdir + set pathinfo [punk::repo::find_repos $scriptroot] + set projectroot [dict get $pathinfo closest] + if {[file exists $scriptroot/wrappers]} { + set customwrapper_folder $scriptroot/wrappers + } else { + #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder if {[string length $projectroot]} { - #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - set customwrapper_folder $projectroot/src/scriptapps/wrappers - } + set customwrapper_folder $projectroot/src/scriptapps/wrappers } else { #outside of any project - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - #no customwrapper folder available - set customwrapper_folder "" - } + set customwrapper_folder "" } - } else { - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false } } else { + if {[file pathtype $filepath_or_scriptset] eq "absolute"} { + return false + } set pathinfo [punk::repo::find_repos $startdir] set projectroot [dict get $pathinfo closest] - if {[string length $projectroot]} { - if {[llength [file split $filepath_or_scriptset]] > 1} { - puts stderr "filepath_or_scriptset looks like a path - but doesn't seem to point to a file" - puts stderr "Ensure you are within a project and use just the name of the scriptset, or pass in the full correct path or relative path to current directory" - puts stderr $usage - return false - } else { - #we've already ruled out empty string - so must have a single element representing scriptset - possibly with file extension - set scriptroot $projectroot/src/scriptapps - set customwrapper_folder $projectroot/src/scriptapps/wrappers - #check something matches the scriptset.. - set something_found "" - if {[file exists $scriptroot/$scriptset]} { - set found_script 1 - set something_found $scriptroot/$scriptset ;#extensionless file - that's ok too - } else { - foreach e $allowed_extensions { - if {[file exists $scriptroot/$scriptset.$e]} { - set found_script 1 - set something_found $scriptroot/$scriptset.$e - break - } + if {![string length $projectroot]} { + puts stderr "No matching scripts or config found for $filepath_or_scriptset, and you are not within a directory where projectroot and src/scriptapps can be determined" + return false + } + + set scriptroot $projectroot/src/scriptapps + set customwrapper_folder $projectroot/src/scriptapps/wrappers + #check something matches the scriptset.. + if {$scriptset ne ""} { + #.toml file may or may not exist + if {[file exists $scriptroot/${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptroot $s] } } - if {!$found_script} { - puts stderr "Searched within $scriptroot" - puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" - puts stderr $usage + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" return false - } else { - if {[file type $something_found] ne "file"} { - puts stderr "Found '$something_found'" - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptroot" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptroot/$scriptset.$e]} { + lappend list_input_files $scriptroot/$scriptset.$e } } } - } else { - puts stderr "filepath_or_scriptset parameter doesn't seem to refer to a file, and you are not within a directory where projectroot and src/scriptapps/wrappers can be determined" - puts stderr $usage - return false + #expect a single script + if {[file exists $scriptroot/$filepath_or_scriptset]} { + if {[file type $scriptroot/$filepath_or_scriptset] ne "file"} { + puts stderr "wrap_in_multishell doesn't currently support a directory as the path. path: $scriptroot/$filepath_or_scriptset" + return false + } + lappend list_input_files $scriptroot/$filepath_or_scriptset + } } - } - #assertion - customwrapper_folder var exists - but might be empty + set found_script [expr {[llength $list_input_files] > 0}] - - if {[string length $ext]} { - #If there was an explicitly supplied extension - then that file should exist - if {![file exists $scriptroot/$scriptset.$ext]} { - puts stderr "Explicit extension .$ext was supplied - but matching file not found." - puts stderr $usage - return false - } else { - if {$ext eq "wrapconfig"} { - set process_extensions ALLFOUNDORCONFIGURED + #---------------------- + if {!$found_script} { + puts stderr "Searched within $scriptdir and $scriptroot" + if {$scriptset ne ""} { + puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" } else { - set process_extensions $ext + puts stderr "Unable to find file $filepath_or_scriptset" } + return false } - } else { - #no explicit extension - process all for scriptset - set process_extensions ALLFOUNDORCONFIGURED + } - #process_extensions - either a single one - or all found or as per .wrapconfig + #assertion - customwrapper_folder var exists - but might be empty - if {$opt_template eq "\uFFFF"} { - set templatename punk.multishell.cmd + if {[dict exists $configd template]} { + set templatename [dict get $configd template] } else { - set templatename $opt_template + if {$opt_template eq "\uFFFF"} { + set templatename punk.multishell.cmd + } else { + set templatename $opt_template + } } set templatename_root [file rootname [file tail $templatename]] @@ -995,7 +1136,7 @@ namespace eval punk::mix::commandset::scriptwrap { set template_base_dict [punk::mix::base::lib::get_template_basefolders] set tpldirs [list] dict for {tdir tsourceinfo} $template_base_dict { - set vendor [dict get $tsourceinfo vendor] + set vendor [dict get $tsourceinfo vendor] if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { lappend tpldirs $tdir } elseif {[file exists $tdir/utility/scriptappwrappers/${templatename_fileroot}[file extension $templatename]]} { @@ -1032,7 +1173,7 @@ namespace eval punk::mix::commandset::scriptwrap { } - if {$opt_outputfolder eq "\uFFFF"} { + if {$opt_outputfolder eq ""} { #outputfolder not explicitly specified by caller if {[string length $projectroot]} { set output_folder [file join $projectroot/bin] @@ -1056,13 +1197,36 @@ namespace eval punk::mix::commandset::scriptwrap { #todo - #output_file extension may also depend on the template being used.. and/or the .wrapconfig - if {$::tcl_platform(platform) eq "windows"} { - set output_extension cmd + #output_file extension may also depend on the template being used.. and/or the _wrap.toml config + + if {[dict size $configd]} { + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other + } + } + set out [dict get $configd $ptype outputfile] + set output_file [file join $output_folder $out] } else { - set output_extension sh + #no _wrap.toml file available + if {$::tcl_platform(platform) eq "windows"} { + set output_extension .cmd + } else { + set output_extension .sh + } + if {$scriptset ne ""} { + set output_file [file join $output_folder $scriptset$output_extension] + } else { + set infile [lindex $list_input_files 0] + set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] + } } - set output_file [file join $output_folder $scriptset.$output_extension] + + if {[file exists $output_file]} { set fdexisting [open $output_file r] fconfigure $fdexisting -translation binary @@ -1103,13 +1267,10 @@ namespace eval punk::mix::commandset::scriptwrap { #foreach ln $template_lines { #} - set list_input_files [list] - if {$process_extensions eq "ALLFOUNDORCONFIGURED"} { - #todo - look for .wrapconfig or all extensions for the scriptset - puts stderr "Sorry - only single input file supported. Supply a file extension or use a .wrapconfig with a single input file for now - implementation incomplete" + if {[llength $list_input_files] > 1} { + #todo + puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" return false - } else { - lappend list_input_files $scriptroot/$scriptset.$ext } #todo - split template at each etc marker and build a dict of parts @@ -1117,7 +1278,6 @@ namespace eval punk::mix::commandset::scriptwrap { #hack - process one input set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] fconfigure $fdscript -translation binary set script_data [read $fdscript] @@ -1131,7 +1291,8 @@ namespace eval punk::mix::commandset::scriptwrap { } puts stdout "-----------------------------------------------\n" puts stdout "Target for above script data is '$output_file'" - set lang [dict get $extension_langs [string tolower $ext]] + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] puts stdout "Language of script being wrapped is $lang" if {$opt_askme} { set answer [util::askuser "Does this look correct? Y|N"] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 6bd826e2..f8e55b02 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -444,9 +444,8 @@ tcl::namespace::eval punk::ns { set nspath [string map {:::: ::} $nspath] set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] - if {[lindex $parts end] eq ""} { - - } + #if {[lindex $parts end] eq ""} { + #} return $parts } @@ -531,6 +530,21 @@ tcl::namespace::eval punk::ns { return [regexp [dict get $ns_re_cache $glob] $path] } + #namespace tree without globbing or weird ns consideration + proc nstree_raw {{location ::}} { + if {![string match ::* $location]} { + error "nstree_raw requires a fully qualified namespace" + } + nstree_rawlist $location + } + proc nstree_rawlist {location} { + set nslist [list $location] + foreach ch [::namespace children $location] { + lappend nslist {*}[nstree_rawlist $ch] + } + return $nslist + } + proc nstree {{location ""}} { if {![string match ::* $location]} { set nscaller [uplevel 1 {::namespace current}] @@ -3899,6 +3913,7 @@ tcl::namespace::eval punk::ns { } proc _pkguse_vars {varnames} { + #review - obsolete? while {"pkguse_vars_[incr n]" in $varnames} {} #return [concat $varnames pkguse_vars_$n] return [list {*}$varnames pkguse_vars_$n] @@ -3932,10 +3947,12 @@ tcl::namespace::eval punk::ns { #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically + variable pkguse_package_to_namespace [dict create] proc pkguse {args} { + variable pkguse_package_to_namespace set argd [punk::args::parse $args withid ::punk::ns::pkguse] lassign [dict values $argd] leaders opts values received - puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + #puts stderr "leaders:$leaders opts:$opts values:$values received:$received" set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] if {[dict exists $received script]} { @@ -3967,68 +3984,159 @@ tcl::namespace::eval punk::ns { set ver "";# tcl version? } default { - if {[string match ::* $pkg_or_existing_ns]} { - set pkg_unqualified [string range $pkg_or_existing_ns 2 end] - if {![tcl::namespace::exists $pkg_or_existing_ns]} { - set ver [package require $pkg_unqualified] - } else { - set ver "" - } + #- comparing namespaces_before vs namespaces_after only works if the package was not previously loaded + #we could either go to the somewhat expensive route of steaming up an interp with the same auto_path & tcl::tm::list each time.. + #or cache the result of the namespace we picked for later pkguse calls (pkguse_package_to_namespace dict) + #we are using the cache method - but this also doesn't help for packages previously loaded by normal package require + #our aim is for pkguse to be deterministic in what namespace it finds - even if it doesn't always get the ideal one (e.g cookiejar, see below) + #To determine appropriate namespace for already loaded packages where we have no cache entry - we may still need the helper interp mechanism + #The helper interp could be persistent - but only so long as the auto_path/tcl::tm::list values are in sync + #review. + + #also see img::png img::raw etc + #these don't directly load namespaces or direct commands.. just change behaviour of existing commands? + #but they can load things like tk (ttk namespace) first one creates ::tkimg? + + if {[string match ::* $pkg_or_existing_ns] && [tcl::namespace::exists $pkg_or_existing_ns]} { + #pkguse on an existing full qualified namespace does no package require set ns $pkg_or_existing_ns + set ver "" } else { - set pkg_unqualified $pkg_or_existing_ns - set ver [package require $pkg_unqualified] - set ns ::$pkg_unqualified - } - #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index - set previous_command_count 0 - if {[namespace exists $ns]} { - set previous_command_count [llength [info commands ${ns}::*]] - } + if {[string match ::* $pkg_or_existing_ns]} { + set pkg_unqualified [string range $pkg_or_existing_ns 2 end] + } else { + set pkg_unqualified $pkg_or_existing_ns + } + #foreach equiv of while 1 - just to allow early exit with break + foreach code_block single { + if {[dict exists $pkguse_package_to_namespace $pkg_unqualified]} { + set ns [dict get $pkguse_package_to_namespace $pkg_unqualified] + set ver [package provide $pkg_unqualified] + break + } + if {[package provide $pkg_unqualified] ne ""} { + #package has already been loaded + if {[namespace exists ::$pkg_unqualified]} { + set ns ::$pkg_unqualified + set ver [package provide $pkg_unqualified] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + break + } + #existing package but no matching namespace.. + #- load in throwaway interp and see what cmds/namespaces created + interp create nstest + try { + nstest eval {tcl::tm::remove {*}[tcl::tm::list]} + nstest eval [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] + nstest eval [list set ::auto_path $::auto_path] + nstest eval {package require punk::ns} + set ns "" + if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] + set ns [nstest eval $script] + } else { + puts "couldn't test pkg $pkg_unqualified\n$errMsg" + } + } finally { + interp delete nstest + } - #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands - #for the purposes of pkguse - which most commonly interactive - we want the namespace populated - #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + set ver [package provide $pkg_unqualified] + break + } - if {!$ns_populated} { - #we will catch-run an auto_index entry if any - #auto_index entry may or may not be prefixed with :: - set keys [list] - #first look for exact pkg_unqualified and ::pkg_unqualified - #leave these at beginning of keys list - if {[array exists ::auto_index($pkg_unqualified)]} { - lappend keys $pkg_unqualified - } - if {[array exists ::auto_index(::$pkg_unqualified)]} { - lappend keys ::$pkg_unqualified - } - #as auto_index is an array - we could get keys in arbitrary order - set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] - lappend keys {*}$matches - set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] - lappend keys {*}$matches - set ns_populated 0 - set i 0 - set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing - set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] - while {!$ns_populated && $i < [llength $keys]} { - #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base - #e.g if we are loading ::x::y - #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc - set k [lindex $keys $i] - set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] - if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { - set auto_source [set ::auto_index($k)] - if {$auto_source ni $already_sourced} { - uplevel 1 $auto_source - lappend already_sourced $auto_source - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + #pkg not loaded + set namespaces_before [nstree_rawlist ::] ;#approx 1ms for 500 or so namespaces - not cheap but bearable + #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index + #gathering prior cmdcount for every ns in system is also a somewhat expensive operation.. review + #we don't know for sure that the namespace for the package require operation actually matches the package name + #e.g tcllib inifile package uses namespace ::ini + #e.g sqlite3 package adds commands to the global namespace + set dict_ns_commandcounts [dict create] + foreach nsb $namespaces_before { + dict set dict_ns_commandcounts $nsb [llength [info commands ${nsb}::*]] + } + + set ver [package require $pkg_unqualified] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + set namespaces_after [nstree_rawlist ::] + + if {[llength $namespaces_after] > [llength $namespaces_before]} { + set namespaces_new [struct::set difference $namespaces_after $namespaces_before] + if {$ns ni $namespaces_new} { + #todo - use shortest result? what if this is a namespace from a required sub package? + #e.g cookiejar loads sqlite3,http,tcl::idna which creates ::sqlite3 etc - but cookiejar just creates an object at ::http::cookiejar + #In this specific case we end up in oo::ObjXXX - but would be better placed in ::http, where the new cookiejar command resides + #review - todo? + set pkgs [package names] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + #find something new - that doesn't match another package name + foreach new $namespaces_new { + if {[lsearch $pkgs [string trimleft $new :]] == -1} { + set ns $new + break + } + } } } - incr i - } + if {[tcl::namespace::exists $ns]} { + #review - only cache if exists? + dict set pkguse_package_to_namespace $pkg_unqualified $ns; + } + set previous_command_count 0 + if {[dict exists $dict_ns_commandcounts $ns]} { + set previous_command_count [dict get $dict_ns_commandcounts $ns] + } + + #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands + #for the purposes of pkguse - which most commonly interactive - we want the namespace populated + #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + + if {!$ns_populated} { + #we will catch-run an auto_index entry if any + #auto_index entry may or may not be prefixed with :: + set keys [list] + #first look for exact pkg_unqualified and ::pkg_unqualified + #leave these at beginning of keys list + if {[array exists ::auto_index($pkg_unqualified)]} { + lappend keys $pkg_unqualified + } + if {[array exists ::auto_index(::$pkg_unqualified)]} { + lappend keys ::$pkg_unqualified + } + #as auto_index is an array - we could get keys in arbitrary order + set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] + lappend keys {*}$matches + set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] + lappend keys {*}$matches + set ns_populated 0 + set i 0 + set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing + set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] + while {!$ns_populated && $i < [llength $keys]} { + #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base + #e.g if we are loading ::x::y + #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc + set k [lindex $keys $i] + set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] + if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { + set auto_source [set ::auto_index($k)] + if {$auto_source ni $already_sourced} { + puts stderr "pkguse sourcing auto_index script $auto_source" + uplevel 1 $auto_source + lappend already_sourced $auto_source + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + } + } + incr i + } + + } + + }; # end foreach code_block single - scope for use of 'break' } } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index a31e255e..fd84ec8d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -3567,7 +3567,6 @@ namespace eval repl { if {[catch { package require punk::args - catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical package require punk::config package require punk::ns #puts stderr "loading natsort" @@ -3589,6 +3588,7 @@ namespace eval repl { }} [punk::config::configure running] package require textblock + catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm index 472edc54..f2f4a3af 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -6007,7 +6007,7 @@ tcl::namespace::eval textblock { proc welcome_test {} { package require punk::ansi - set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] + set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/ROY-WELC.ANS 80x8]] # Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index 2de13afb..37f36a9a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl @@ -22,7 +22,7 @@ namespace eval ::punkboot { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /? -h] - variable known_commands [list project modules vfs info check shell vendorupdate bootsupport vfscommonupdate ] + variable known_commands [list project modules libs packages vfs bin info check shell vendorupdate bootsupport vfscommonupdate ] } @@ -1077,10 +1077,16 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - This help." \n \n append h " $scriptname project ?-k?" \n append h " - this is the literal word project - and confirms you want to run the project build - which includes src/vfs/* checks and builds" \n - append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n - append h " - built modules go into /modules /lib etc." \n \n + append h " - the optional -k flag will terminate running processes matching the executable being built (if applicable)" \n + append h " - builds/copies .tm modules from src to /modules etc and pkgIndex.tcl based libraries from src to /lib etc." \n \n append h " $scriptname modules" \n - append h " - build modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " - build (or copy if build not required) .tm modules from src/modules src/vendormodules etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname libs" \n + append h " - build (or copy if build not required) pkgIndex.tcl based libraries from src/lib src/vendorlib etc to their corresponding locations under " \n + append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n + append h " $scriptname packages" \n + append h " - build (or copy if build not required) both .tm and pkgIndex.tcl based packages from src to their corresponding locations under " \n append h " This does not scan src/runtime and src/vfs folders to build kit/zipkit/cookfs executables" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n @@ -1089,6 +1095,7 @@ proc ::punkboot::punkboot_gethelp {args} { append h " - bootsupport modules are available to make.tcl" \n \n append h " $scriptname vendorupdate" \n append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n + append h " - update the src/vendorlib based on src/vendorlib/config.toml (todo)" \n \n append h " $scriptname vfscommonupdate" \n append h " - update the src/vfs/_vfscommon.vfs from compiled src/modules and src/lib etc" \n append h " - before calling this (followed by make project) - you can test using '(.exe) dev'" \n @@ -1213,6 +1220,8 @@ if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} } set sourcefolder $projectroot/src +set binfolder $projectroot/bin + if {$::punkboot::command eq "check"} { set sep [string repeat - 75] puts stdout $sep @@ -1348,8 +1357,8 @@ if {$::punkboot::command eq "info"} { puts stdout "- -- --- --- --- --- --- --- --- --- -- -" puts stdout "- projectroot : $projectroot" set sourcefolder $projectroot/src - set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] - set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] + set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] + set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] puts stdout "- vendorlib folders: ([llength $vendorlibfolders])" foreach fld $vendorlibfolders { puts stdout " src/$fld" @@ -1358,13 +1367,18 @@ if {$::punkboot::command eq "info"} { foreach fld $vendormodulefolders { puts stdout " src/$fld" } - set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] - puts stdout "- source module paths: [llength $source_module_folderlist]" - foreach fld $source_module_folderlist { + #set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] ;#returns only those containing .tm files + #foreach fld $source_module_folderlist { + # set relpath [punkcheck::lib::path_relative $projectroot $fld] + # puts stdout " $relpath" + #} + set projectmodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails modules_tcl* modules] + puts stdout "- source module paths: [llength $projectmodulefolders]" + #JJJ + foreach fld $projectmodulefolders { puts stdout " $fld" } - set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] - lappend projectlibfolders lib + set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl* lib] puts stdout "- source libary paths: [llength $projectlibfolders]" foreach fld $projectlibfolders { puts stdout " src/$fld" @@ -1759,7 +1773,7 @@ if {$::punkboot::command eq "bootsupport"} { -if {$::punkboot::command ni {project modules vfs}} { +if {$::punkboot::command ni {project modules libs packages vfs bin}} { puts stderr "Command $::punkboot::command not implemented - aborting." flush stderr after 100 @@ -1772,7 +1786,7 @@ if {$::punkboot::command ni {project modules vfs}} { #install src vendor contents (from version controlled src folder) to base of project (same target folders as our own src/modules etc ie to paths that go on the auto_path and in tcl::tm::list) -if {$::punkboot::command in {project modules}} { +if {$::punkboot::command in {project packages modules}} { set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules vendormodules_tcl*] foreach vf $vendormodulefolders { lassign [split $vf _] _vm tclx @@ -1797,7 +1811,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendormodulefolders]} { puts stderr "VENDORMODULES: No src/vendormodules or src/vendormodules_tcl* folders found." } +} +if {$::punkboot::command in {project packages libs}} { set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib vendorlib_tcl*] foreach lf $vendorlibfolders { lassign [split $lf _] _vm tclx @@ -1827,8 +1843,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $vendorlibfolders]} { puts stderr "VENDORLIB: No src/vendorlib or src/vendorlib_tcl* folder found." } +} - +if {$::punkboot::command in {project packages modules libs}} { ######################################################## #templates #e.g The default project layout is mainly folder structure and readme files - but has some scripts developed under the main src that we want to sync @@ -1896,6 +1913,9 @@ if {$::punkboot::command in {project modules}} { $tpl_installer destroy } } +} + +if {$::punkboot::command in {project packages libs}} { ######################################################## set projectlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails lib_tcl*] lappend projectlibfolders lib @@ -1927,7 +1947,9 @@ if {$::punkboot::command in {project modules}} { if {![llength $projectlibfolders]} { puts stderr "PROJECTLIB: No src/lib or src/lib_tcl* folder found." } +} +if {$::punkboot::command in {project packages modules}} { #consolidated /modules /modules_tclX folder used for target where X is tcl major version #the make process will process for any _tclX not just the major version of the current interpreter @@ -1964,9 +1986,10 @@ if {$::punkboot::command in {project modules}} { ] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } +} +if {$::punkboot::command in {project packages modules libs}} { set installername "make.tcl" - # ---------------------------------------- if {[punk::repo::is_fossil_root $projectroot]} { set config [dict create\ @@ -2013,7 +2036,7 @@ if {$::punkboot::command in {project modules}} { #review set installername "make.tcl" -if {$::punkboot::command ni {project vfs}} { +if {$::punkboot::command ni {project vfs bin}} { #command = modules puts stdout "vfs folders not checked" puts stdout " - use 'make.tcl vfscommonupdate' to copy built modules into base vfs folder" @@ -2033,6 +2056,17 @@ if {$buildfolder ne "$sourcefolder/_build"} { exit 2 } +if {$::punkboot::command eq "bin"} { + puts stdout "checking $sourcefolder/bin" + set resultdict [punkcheck::install $sourcefolder/bin $binfolder\ + -overwrite synced-targets\ + -installer "punkboot-bin"\ + -progresschannel stdout\ + ] + + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + flush stdout +} #find runtimes set rtfolder $sourcefolder/runtime @@ -2056,11 +2090,32 @@ if {![llength $runtimes]} { } set has_sdx 1 -if {[catch {exec sdx help} errM]} { - puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" - puts stderr "err: $errM" - #exit 1 - set has_sdx 0 +set sdxpath [auto_execok $binfolder/sdx] +if {$sdxpath eq ""} { + set sdxpath [auto_execok [file dirname [info nameofexecutable]]/sdx] + if {$sdxpath eq ""} { + #last resort - look on path + set sdxpath [auto_execok sdx] + } + if {$sdxpath eq ""} { + #last resort - a tclkit and sdx.kit fine + if {[file exists $binfolder/sdx.kit]} { + set tclkitpath [auto_execok $binfolder/tclkit] + if {$tclkitpath eq ""} { + set tclkitpath [auto_execok tclkit] + } + set sdxpath [list {*}$tclkitpath $binfolder/sdx.kit] + } + } + + if {$sdxpath eq "" || [catch {exec {*}$sdxpath help} errM]} { + puts stderr "FAILED to find usable sdx command or tclkit executable with sdx.bat" + puts stderr "If tclkit-based runtimes are required - check that sdx executable is in bin folder of project or in same folder as tcl/punk executable or on path" + puts stderr "This is not a problem if tcl8.7/tcl9+ kits using the preferred method 'zipfs' are to be used, or if cookfs based kits are to be used." + puts stderr "err: $errM" + #exit 1 + set has_sdx 0 + } } # -- --- --- --- --- --- --- --- --- --- @@ -2825,17 +2880,17 @@ foreach vfstail $vfs_tails { if {[catch { if {$rtname ne "-"} { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $building_runtime {*}$verbose } else { - exec sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose + exec {*}$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose } } result]} { if {$rtname ne "-"} { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs -runtime $buildfolder/build_$runtime_fullname {*}$verbose failed with msg: $result" } else { - set sdxmsg "sdx wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" + set sdxmsg "$::sdxpath wrap $buildfolder/$vfsname.new -vfs $wrapvfs {*}$verbose failed with msg: $result" } - puts stderr "sdx wrap $targetkit failed" + puts stderr "$::sdxpath wrap $targetkit failed" lappend failed_kits [list kit $targetkit reason $sdxmsg] $vfs_event targetset_end FAILED $vfs_event destroy @@ -3022,6 +3077,7 @@ foreach vfstail $vfs_tails { } ;#end foreach rtname in runtimes # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- } + cd $startdir if {[llength $installed_kits]} { puts stdout "INSTALLED KITS: ([llength $installed_kits])" diff --git a/src/runtime/mapvfs.config b/src/runtime/mapvfs.config index 1af6958f..6ed5f1f3 100644 --- a/src/runtime/mapvfs.config +++ b/src/runtime/mapvfs.config @@ -53,7 +53,7 @@ tclsh90magic.exe {punk9magicsplat.vfs punkmagic zipcat} #we would require compiled cookfs extension to extract existing vfs from a cookit, or if we wanted to re-write as cookfs #(possibly upx binary too if compressed - upx is easily attainable on most platforms) -#cookitU.exe {punk9cook.vfs punk9cook cookfs} +cookitU.exe {punk9cook.vfs punk9cook cookfs} #cookitU.exe {punk9cook.vfs punk9cz zip} ################################## diff --git a/src/scriptapps/example.sh b/src/scriptapps/example.sh new file mode 100644 index 00000000..3f0f1e40 --- /dev/null +++ b/src/scriptapps/example.sh @@ -0,0 +1 @@ +echo "output from example.sh wrapped in polyglot script" \ No newline at end of file diff --git a/src/scriptapps/example.tcl b/src/scriptapps/example.tcl new file mode 100644 index 00000000..cd3796d2 --- /dev/null +++ b/src/scriptapps/example.tcl @@ -0,0 +1 @@ +puts stdout "output from example.tcl wrapped in polyglot script" \ No newline at end of file diff --git a/src/scriptapps/example_out.bat b/src/scriptapps/example_out.bat new file mode 100644 index 00000000..c45adc6c --- /dev/null +++ b/src/scriptapps/example_out.bat @@ -0,0 +1,743 @@ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ +: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + +: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. +: shebang line is not required on unix or windows and will reduce functionality and/or portability. +: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. +@GOTO :skip_perl_pod_start ^; +=begin excludeperl +: skip_perl_pod_start +: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ +: { +@REM ############################################################################################################################ +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. +@REM ############################################################################################################################ +@REM On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder +@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) +@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called. +@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. +@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. +@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context +@SETLOCAL EnableExtensions EnableDelayedExpansion +@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________" +@REM for batch - only win32 is relevant - but other scripts on other platforms also parse the nextshell block to determine next shell to launch +@REM nextshellpath and nextshelltype indices (underscore-padded to 16wide) are "other" plus those returned by Tcl platform pkg e.g win32,linux,freebsd,macosx +@REM The horrible underscore-padded fixed-widths are to keep the batch labels aligned whilst allowing values to be set +@REM If more than 32 chars needed for a target, it can still be done but overall script padding may need checking/adjusting +@REM Supporting more explicit oses than those listed may also require script padding adjustment +: +@SET "nextshellpath[win32___________]=tclsh___________________________" +@SET "nextshelltype[win32___________]=tcl_____________" +@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" +@SET "nextshelltype[dragonflybsd____]=tcl_____________" +@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[freebsd_________]=tcl_____________" +@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[netbsd__________]=tcl_____________" +@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[linux___________]=tcl_____________" +@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[macosx__________]=tcl_____________" +@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[other___________]=tcl_____________" +: +@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). +: +@SET "asadmin=0" +: +@REM @ECHO nextshelltype is %nextshelltype[win32___________]% +@REM @SET "selected_shelltype=%nextshelltype[win32___________]%" +@SET "selected_shelltype=%nextshelltype[win32___________]%" +@REM @ECHO selected_shelltype %selected_shelltype% +@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed +@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed% +@SET "selected_shellpath=%nextshellpath[win32___________]%" +@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed +@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%" +@REM @ECHO keyremoved %keyRemoved% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix but should be left in place) +@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) +@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. +@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 +@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. +@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 +@REM ############################################################################################################################ +@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) +@REM -- Even something as simple as adding or removing an @REM +@REM -- From within punkshell - use: +@REM -- deck scriptwrap.checkfile +@REM -- to check your templates or final wrapped scripts for byte boundary issues +@REM -- It will report any labels that are on boundaries +@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. +@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- Alternatively, as you should do anyway - test the final script on windows +@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. +@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. +@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label +@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. +@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided +@REM ############################################################################################################################ +@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SET "winpath=%~dp0" +@SET "fname=%~nx0" +@REM @ECHO fname %fname% +@REM @ECHO winpath %winpath% +@REM @ECHO commandlineascalled %0 +@REM @ECHO commandlineresolved %~f0 +@CALL :getNormalizedScriptTail nftail +@REM @ECHO normalizedscripttail %nftail% +@CALL :getFileTail %0 clinetail +@REM @ECHO clinetail %clinetail% +@CALL :stringToUpper %~nx0 capscripttail +@REM @ECHO capscriptname: %capscripttail% + +@IF "%nftail%"=="%capscripttail%" ( + @ECHO forcing asadmin=1 due to file name on filesystem being uppercase + @SET "asadmin=1" +) else ( + @CALL :stringToUpper %clinetail% capcmdlinetail + @REM @ECHO capcmdlinetail !capcmdlinetail! + IF "%clinetail%"=="!capcmdlinetail!" ( + @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase + @set "asadmin=1" + ) +) +@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" +@SET arglist=%* +@SET "qstrippedargs=args%arglist%" +@SET "qstrippedargs=%qstrippedargs:"=%" +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( + GOTO :gotPrivileges +) +@IF !asadmin!==1 ( + net file 1>NUL 2>NUL + @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) +) +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@GOTO skip_privileges +:getPrivileges +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) +@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%" +@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%" +@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%" +@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%" +@ECHO Next >> "%vbsGetPrivileges%" +@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" +@ECHO Launching script in new windows due to administrator elevation +@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* +@EXIT /B + +:gotPrivileges +@REM setlocal & pushd . +@PUSHD . +@cd /d %~dp0 +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( + @DEL "%vbsGetPrivileges%" 1>nul 2>nul + @SET arglist=%arglist:~14% +) + +:skip_privileges +@SET need_ps1=0 +@REM we want the ps1 to exist even if the nextshell isn't powershell +@if not exist "%~dp0%~n0.ps1" ( + @SET need_ps1=1 +) ELSE ( + fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different + @REM @ECHO "files same" + @SET need_ps1=0 +) +@GOTO :pscontinue +:different +@REM @ECHO "files differ" +@SET need_ps1=1 +:pscontinue +@IF !need_ps1!==1 ( + COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL +) +@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /? +@IF "%selected_shelltype_trimmed%"=="powershell" ( + REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time + REM test availability of preferred option of powershell7+ pwsh + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + REM fallback to powershell if pwsh failed + IF !pwshtest_exitcode!==0 ( + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM CALL powershell -nop -nol -c write-host powershell-found + REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* + powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF "%selected_shelltype_trimmed%"=="wslbash" ( + CALL :getWslPath %winpath% wslpath + REM ECHO wslfullpath "!wslpath!%fname%" + %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM perl or tcl or sh or bash + IF NOT "x%keyRemoved%"=="x%validshelltypes%" ( + REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl + REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx + REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode + %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call; + ) ELSE ( + ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes% + SET task_exitcode=66 + @REM boundary padding + @REM boundary padding + @REM boundary padding + @REM boundary padding + GOTO :exit_multishell + ) + ) +) +@REM batch file library functions +@REM boundary padding +@GOTO :endlib + +:getWslPath +@SETLOCAL + @SET "_path=%~p1" + @SET "name=%~nx1" + @SET "drive=%~d1" + @SET "rtrn=%~2" + @REM Although drive letters on windows are normally upper case wslbash seems to expect lower case drive letters + @CALL :stringToLower %drive ldrive + @SET "result=/mnt/%ldrive:~0,1%%_path:\=/%%name%" +@ENDLOCAL & ( + @if "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + ECHO %result% + ) +) +@EXIT /B + +:getFileTail +@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd +@REM we can't use things such as %~nx1 as it can change capitalisation +@REM This function is designed explicitly to preserve capitalisation +@REM accepts full paths with either / or \ as delimiters - or +@SETLOCAL + @SET "rtrn=%~2" + @SET "arg=%~1" + @REM @SET "result=%_arg:*/=%" + @REM @SET "result=%~1" + @SET LF=^ + + + : The above 2 empty lines are important. Don't remove + @CALL :stringContains "!arg!" "\" hasBackSlash + @IF "!hasBackslash!"=="true" ( + @for %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @CALL :stringContains "!arg!" "/" hasForwardSlash + @IF "!hasForwardSlash!"=="true" ( + @FOR %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @set "result=%arg%" + ) + ) +@ENDLOCAL & ( + @if "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:getNormalizedScriptTail +@SETLOCAL + @SET "result=%~nx0" + @SET "rtrn=%~1" +@ENDLOCAL & ( + @IF "%~1" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B + +:getNormalizedFileTailFromPath +@REM warn via echo, and do not set return variable if path not found +@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' +@REM boundary padding +@REM boundary padding +@REM boundary padding +@REM boundary padding +@SETLOCAL + @CALL :stringContains %~1 "\" hasBackSlash + @CALL :stringContains %~1 "/" hasForwardSlash + @IF "%hasBackslash%-%hasForwardslash%"=="false-false" ( + @SET "P=%cd%%~1" + @CALL :getNormalizedFileTailFromPath "!P!" ftail2 + @SET "result=!ftail2!" + ) else ( + @IF EXIST "%~1" ( + @SET "result=%~nx1" + ) else ( + @ECHO error getNormalizedFileTailFromPath file not found: %~1 + @EXIT /B 1 + ) + ) + @SET "rtrn=%~2" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + @ECHO getNormalizedFileTailFromPath %1 result: %result% + ) +) +@EXIT /B + +:stringContains +@REM usage: @CALL:stringContains string needle returnvarname +@SETLOCAL + @SET "rtrn=%~3" + @SET "string=%~1" + @SET "needle=%~2" + @IF "!string:%needle%=!"=="!string!" @( + @SET "result=false" + ) ELSE ( + @SET "result=true" + ) +@ENDLOCAL & ( + @IF "%~3" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringContains %string% %needle% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:stringToUpper +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "capstring=%~1" + @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @( + @SET "capstring=!capstring:%%A=%%A!" + ) + @SET "result=!capstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToUpper %string% result: %result% + ) +) +@EXIT /B +:stringToLower +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "retstring=%~1" + @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( + @SET "retstring=!retstring:%%A=%%A!" + ) + @SET "result=!retstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToLower %string% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:stringTrimTrailingUnderscores +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "trimstring=%~1" + @REM trim up to 31 underscores from the end of a string using string substitution + @SET trimstring=%trimstring%### + @SET trimstring=%trimstring:________________###=###% + @SET trimstring=%trimstring:________###=###% + @SET trimstring=%trimstring:____###=###% + @SET trimstring=%trimstring:__###=###% + @SET trimstring=%trimstring:_###=###% + @SET trimstring=%trimstring:###=% + @SET "result=!trimstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringTrimTrailingUnderscores %string% result: %result% + ) +) +@EXIT /B +:isNumeric +@SETLOCAL + @SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i" + @IF defined notnumeric ( + @SET "result=false" + ) else ( + @SET "result=true" + ) + @SET "rtrn=%~2" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B + +:endlib +: \ +@REM padding +@REM padding +@REM @SET taskexit_code=!errorlevel! & goto :exit_multishell +@GOTO :exit_multishell +# } +# -*- tcl -*- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- tcl script section +# -- This is a punk multishell file +# -- Primary payload target is Tcl, with sh,bash,powershell as helpers +# -- but it may equally be used with any of these being the primary script. +# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script +# -- i.e it is a polyglot file. +# -- The specific layout including some lines that appear just as comments is quite sensitive to change. +# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline. +# -- e.g ./filename.polypunk.cmd in sh or bash +# -- e.g tclsh filename.cmd +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore +Hide :exit_multishell;Hide {<#};Hide '@ +namespace eval ::punk::multishell { + set last_script_root [file dirname [file normalize ${::argv0}/__]] + set last_script [file dirname [file normalize [info script]/__]] + if {[info exists ::argv0] && + $last_script eq $last_script_root + } { + set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode + } else { + set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. + } + if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { + proc ::punk::multishell::is_main {{script_name {}}} { + if {$script_name eq ""} { + set script_name [file dirname [file normalize [info script]/--]] + } + if {![info exists ::punk::multishell::is_main($script_name)]} { + #e.g a .dll or something else unanticipated + puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting" + puts stderr "Info: script_root: [file dirname [file normalize ${::argv0}/__]]" + return 0 + } + return [set ::punk::multishell::is_main($script_name)] + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" +#puts "argv0 : $::argv0" +# -- --- --- --- --- --- --- --- --- --- --- --- + + +# +# + +# +# + + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- Best practice is to always return or exit above, or just by leaving the below defaults in place. +# -- If the multishell script is modified to have Tcl below the Tcl Payload section, +# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. +# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below +# -- but the sh/bash 'then' and 'fi' would also need to be uncommented. +# -- This facility left in place for experiments on whether configuration payloads etc can be appended +# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells +# -- can be made to ignore/cope with such data. +if {[::punk::multishell::is_main]} { + exit 0 +} else { + return +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +# end hide from unix shells \ +HEREDOC1B_HIDE_FROM_BASH_AND_SH +# sh/bash \ +shift && set -- "${@:1:$#-1}" +#------------------------------------------------------ +# -- This if block only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- sh/bash script section +# -- leave as is if all that is required is launching the Tcl payload" +# -- +# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default +# -- adjust the %nextshell% value above +# -- if sh/bash scripting needs to run on windows too. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +exitcode=0 +#printf "start of bash or sh code" + +# +echo "output from example.sh wrapped in polyglot script" +# + +# -- --- --- --- --- --- --- --- +# +#-- sh/bash launches Tcl here instead of shebang line at top +#-- use exec to use exitcode (if any) directly from the tcl script +#exec /usr/bin/env tclsh "$0" "$@" +#-- alternative - can run sh/bash script after the tcl call. +/usr/bin/env tclsh "$0" "$@" +exitcode=$? +#echo "sh/bash reporting tcl exitcode: ${exitcode}" +#-- override exitcode example +#exit 66 +# +# -- --- --- --- --- --- --- --- + +# +# + + +#printf "sh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload +#------------------------------------------------------ +fi +exit ${exitcode} +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- Perl script section +# -- leave the script below as is, if all that is required is launching the Tcl payload" +# -- +# -- Note that perl script isn't called by default when simply running this script by name +# -- adjust the nextshell value at the top of the script to point to perl +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +=cut +#!/user/bin/perl +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload +my $exit_code = 0; +#use ExtUtils::Installed; +#my $installed = ExtUtils::Installed->new(); +#my @modules = $installed->modules(); +#print "Modules:\n"; +#foreach my $m (@modules) { +# print "$m\n"; +#} +# -- --- --- + + + +my $scriptname = $0; +print "perl $scriptname\n"; +my $i =1; +foreach my $a(@ARGV) { + print "Arg # $i: $a\n"; +} + +# +# + + + +# -- --- --- --- --- --- --- --- +# +$exit_code=system("tclsh", $scriptname, @ARGV); +#print "perl reporting tcl exitcode: $exit_code"; +# +# -- --- --- --- --- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload +exit $exit_code; +__END__ + +# end hide sh/bash/perl block from Tcl +# This comment with closing brace should stay in place whether if commented or not } +#------------------------------------------------------ +# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above +if 0 { +: end heredoc1 - end hide from powershell \ +'@ +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- powershell/pwsh section +# -- Do not edit if current file is the .ps1 +# -- Edit the corresponding .cmd and it will autocopy +# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above +# -- custom script should generally go below the begin_powershell_payload line +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +function GetScriptName { $myInvocation.ScriptName } +$scriptname = GetScriptName +function GetDynamicParamDictionary { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + [string] $CommandName + ) + + begin { + # Get a list of params that should be ignored (they're common to all advanced functions) + $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + } + + process { + # Create the dictionary that this scriptblock will return: + $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Convert to object array and get rid of Common params: + (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | + Where-Object { $CommonParameterNames -notcontains $_.Key } | + ForEach-Object { + $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( + $_.Key, + $_.Value.ParameterType, + $_.Value.Attributes + ) + $DynParamDictionary.Add($_.Key, $DynamicParameter) + } + + # Return the dynamic parameters + return $DynParamDictionary + } +} +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargumentname' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# #... +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargumentname value is: $myargumentname" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +#"Script Name : {0}" -f $scriptname | write-host +#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +#"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- +# +tclsh $scriptname $args +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host +# +# -- --- --- --- --- --- --- --- + + +# +# + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload +Exit $LASTEXITCODE +# heredoc2 for powershell to ignore block below +$1 = @' +' +: comment end hide powershell-block from Tcl \ +# This comment with closing brace should stay in place whether 'if' commented or not } +: multishell doubled-up cmd exit label - return exitcode +:exit_multishell +:exit_multishell +: \ +@REM @ECHO exitcode: !task_exitcode! +: \ +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) +: \ +@EXIT /B !task_exitcode! +# cmd has exited +: comment end heredoc2 \ +'@ +<# +# id:tailblock0 +# -- powershell multiline comment +#> +<# +no script engine should try to run me +# id:tailblock1 +# + +# +# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A) +# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data +# -- so for example a plain text tar archive could cause problems depending on the content. +# -- final line in file must be the powershell multiline comment terminator or other data it can handle. +# -- e.g plain # comment lines will work too +# -- (for example a powershell digital signature is a # commented block of data at the end of the file) +#> + + diff --git a/src/scriptapps/example_wrap.toml b/src/scriptapps/example_wrap.toml new file mode 100644 index 00000000..3e7b0d62 --- /dev/null +++ b/src/scriptapps/example_wrap.toml @@ -0,0 +1,41 @@ +[application] + template="punk.multishell.cmd" + + #scripts=[ + # "example.sh", + # "example.tcl" + #] + scripts=[ + "example.sh" + ] + + default_outputfile="example_out.sh" + default_nextshellpath="/usr/bin/env tclsh" + default_nextshelltype="tcl" + + + #valid nextshelltype entries are: tcl perl powershell bash. + #nextshellpath entries must be 32 characters or less. + + win32.nextshellpath="tclsh" + win32.nextshelltype="tcl" + win32.outputfile="example_out.bat" + + dragonflybsd.nextshellpath="/usr/bin/env tclsh" + dragonflybsd.nextshelltype="tcl" + + freebsd.nextshellpath="/usr/bin/env tclsh" + freebsd.nextshelltype="tcl" + + netbsd.nextshellpath="/usr/bin/env tclsh" + netbsd.nextshelltype="tcl" + + linux.nextshellpath="/usr/bin/env tclsh" + linux.nextshelltype="tcl" + + macosx.nextshellpath="/usr/bin/env tclsh" + macosx.nextshelltype="tcl" + + other.nextshellpath="/usr/bin/env tclsh" + other.nextshelltype="tcl" + diff --git a/src/vfs/_config/punk_main.tcl b/src/vfs/_config/punk_main.tcl index 6ecce171..8081acfe 100644 --- a/src/vfs/_config/punk_main.tcl +++ b/src/vfs/_config/punk_main.tcl @@ -43,7 +43,7 @@ apply { args { set normexe [file dirname [file normalize [file join [info nameofexecutable] __dummy__]]] - puts stderr "STARKIT: [package provide starkit]" + #puts stderr "STARKIT: [package provide starkit]" set topdir [file dirname $normscript] set found_starkit_tcl 0 @@ -60,10 +60,10 @@ apply { args { #package versions does not always return versions in increasing order! if {[set starkitv [lindex [lsort -command {package vcompare} [package versions starkit]] end]] ne ""} { #run the ifneeded script for the latest found (assuming package versions ordering is correct) - puts "111 autopath: $::auto_path" + #puts "111 autopath: $::auto_path" eval [package ifneeded starkit $starkitv] set found_starkit_tcl 1 - puts "222 autopath: $::auto_path" + #puts "222 autopath: $::auto_path" } if {!$found_starkit_tcl} { #our internal 'quick' search for starkit failed. @@ -263,8 +263,8 @@ apply { args { #(differences in boot.tcl in the kits) if {[llength $package_modes] > 1} { - puts stderr "main.tcl PACKAGE MODE is preferencing libraries and modules in the order: $package_modes" - puts stderr "main.tcl original auto_path: $::auto_path" + #puts stderr "main.tcl PACKAGE MODE is preferencing libraries and modules in the order: $package_modes" + #puts stderr "main.tcl original auto_path: $::auto_path" #------------------------------------------------------------------------------ @@ -614,8 +614,8 @@ apply { args { } } } - puts stderr "main.tcl internal_paths: $internal_paths" - puts stderr "main.tcl filtered_auto_path: $filtered_auto_path" + #puts stderr "main.tcl internal_paths: $internal_paths" + #puts stderr "main.tcl filtered_auto_path: $filtered_auto_path" set filtered_tm_list [list] foreach tm [tcl::tm::list] { @@ -700,8 +700,8 @@ apply { args { } #force rescan #catch {package require flobrudder666_nonexistant} - puts stderr "main.tcl auto_path :$::auto_path" - puts stderr "main.tcl tcl::tm::list:[tcl::tm::list]" + #puts stderr "main.tcl auto_path :$::auto_path" + #puts stderr "main.tcl tcl::tm::list:[tcl::tm::list]" } if {1 || $has_zipfs_attached} { diff --git a/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl b/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl index 95f057bb..0508bafe 100644 --- a/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl +++ b/src/vfs/_vfscommon.vfs/lib/app-shellspy/shellspy.tcl @@ -88,11 +88,24 @@ namespace eval shellspy { return [expr {[clock millis]/1000.0}] } variable shellspy_status_log "shellspy-[clock micros]" - set debug_syslog_server 127.0.0.1:514 - #set debug_syslog_server 172.16.6.42:51500 - #set debug_syslog_server "" - set error_syslog_server 127.0.0.1:514 - set data_syslog_server 127.0.0.1:514 + + #todo - default to no logging not even to local syslog + #load a .toml config which can configure logging as desired + set do_log 0 + if {$do_log} { + set debug_syslog_server 127.0.0.1:514 + #set debug_syslog_server 172.16.6.42:51500 + #set debug_syslog_server "" + set error_syslog_server 127.0.0.1:514 + set data_syslog_server 127.0.0.1:514 + } else { + set debug_syslog_server "" + set error_syslog_server "" + set data_syslog_server "" + } + + + shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog $debug_syslog_server -file ""] shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" @@ -570,8 +583,9 @@ namespace eval shellspy { proc do_script_process {scriptbin scriptname args} { variable shellspy_status_log shellfilter::log::write $shellspy_status_log "do_script_process got scriptname:'$scriptname' args:'$args'" - set args [do_callback script_process {*}$args] - set params [do_callback_parameters script_process] + #no script_process callbacks + #set args [do_callback script_process {*}$args] + #set params [do_callback_parameters script_process] dict set params -teehandle shellspy set params [dict merge $params [get_channel_config $::testconfig]] @@ -620,7 +634,7 @@ namespace eval shellspy { proc do_script {scriptname replwhen args} { #ideally we don't want to launch an external process to run the script variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" + #shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" set exepath [file dirname [file join [info nameofexecutable] __dummy__]] set exedir [file dirname $exepath] @@ -651,7 +665,7 @@ namespace eval shellspy { set modulesdir $basedir/modules set script [string map [list %a% $args %s% $scriptpath %m% $modulesdir] { -::tcl::tm::add %m% +#::tcl::tm::add %m% set scriptname %s% set normscript [file normalize $scriptname] @@ -696,9 +710,10 @@ dict with prevglobal {} #just the script } + #no script callbacks + #set args [do_callback script {*}$args] + #set params [do_callback_parameters script] - set args [do_callback script {*}$args] - set params [do_callback_parameters script] dict set params -tclscript 1 ;#don't give callback a chance to omit/break this dict set params -teehandle shellspy #dict set params -teehandle punksh @@ -716,7 +731,8 @@ dict with prevglobal {} # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" #} - shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" + #jjj + #shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo" if {[dict exists $exitinfo errorInfo]} { #strip out the irrelevant info from the errorInfo - we don't want info beyond 'invoked from within' as this is just plumbing related to the script sourcing set stacktrace [string map [list \r\n \n] [dict get $exitinfo errorInfo]] @@ -730,7 +746,8 @@ dict with prevglobal {} } set output [string trimright $output \n] dict set exitinfo errorInfo $output - shellfilter::log::write $shellspy_status_log "do_script simplified exitinfo: $exitinfo" + #jjj + #shellfilter::log::write $shellspy_status_log "do_script simplified exitinfo: $exitinfo" } return $exitinfo } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm index a4f56010..1b15d45a 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm @@ -890,10 +890,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - puts stderr "--> pkg epoch $prev_e -> $current_e" - puts stderr "args: $args" - puts stderr "last_auto: $last_auto_path" - puts stderr "auto_path: $auto_path" + #puts stderr "--> pkg epoch $prev_e -> $current_e" + #puts stderr "args: $args" + #puts stderr "last_auto: $last_auto_path" + #puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 8ef36e27..06b145de 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -20,7 +20,7 @@ #[manpage_begin punkshell_module_scriptwrap 0 0.1.0] #[copyright "2024"] #[titledesc {scriptwrap polyglot tool}] [comment {-- Name section and table of contents description --}] -#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] +#[moddesc {scriptwrap tool}] [comment {-- Description at end of page heading --}] #[require punk::mix::commandset::scriptwrap] #[keywords module commandset launcher scriptwrap] #[description] @@ -30,7 +30,7 @@ #*** !doctools #[section Overview] -#[para] overview of scriptwrap +#[para] overview of scriptwrap #[subsection Concepts] #[para] - @@ -74,7 +74,7 @@ package require punk::fileline namespace eval punk::mix::commandset::scriptwrap { #*** !doctools #[subsection {Namespace punk::mix::commandset::scriptwrap}] - #[para] Core API functions for punk::mix::commandset::scriptwrap + #[para] Core API functions for punk::mix::commandset::scriptwrap #[list_begin definitions] namespace export * @@ -93,7 +93,7 @@ namespace eval punk::mix::commandset::scriptwrap { foreach k [lreverse [dict keys $tdict_low_to_high]] { dict set tdict $k [dict get $tdict_low_to_high $k] } - + #set pathinfolist [dict values $tdict] set names [dict keys $tdict] @@ -142,9 +142,9 @@ namespace eval punk::mix::commandset::scriptwrap { put stderr "commandset::scriptwrap::templates_dict WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } return - } - - + } + + #A batch file with unix line-endings is sensitive to label positioning. #batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it. #see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings) @@ -808,176 +808,317 @@ namespace eval punk::mix::commandset::scriptwrap { return $result } #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site - #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf - proc multishell {filepath_or_scriptset args} { - set opts [dict create\ - -askme 1\ - -outputfolder "\uFFFF"\ - -template "\uFFFF"\ - -returnextra 0\ - -force 0\ - ] - #set known_opts [dict keys $defaults] - foreach {k v} $args { - switch -- $k { - -askme - -outputfolder - -template - -returnextra - -force { - dict set opts $k $v - } - default { - error "punk::mix::commandset::multishell error. Unrecognized option '$k'. Known-options: [dict keys $opts]" - } + #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf + #set usage "" + #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n + #append usage "The scriptset name will be used to search for .sh|.tcl|.ps1 or names as you specify in yourname.wrapconfig if it exists" \n + #append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n + #if {![string length $filepath_or_scriptset]} { + # puts stderr "No filepath_or_scriptset specified" + # puts stderr $usage + # return false + #} + proc _read_scriptset_wrap_tomlfile {fname} { + set resultd [dict create] + package require tomlish + set tomldata [readFile $fname] + #todo - fix tomlish to provide line number in ERROR structure during from_toml call. + if {[catch {tomlish::to_dict [tomlish::from_toml $tomldata]} tomldict]} { + puts stderr "Failed to parse $fname" + puts stderr "error: $tomldict" + } + if {[tomlish::dict::path::exists $tomldict {.application.template}]} { + dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] + } + set scripts [list] + if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { + set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] + foreach tvdict $arrvalues { + lappend scripts [dict get $tvdict value] } } + dict set resultd scripts $scripts - set usage "" - append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n - append usage "The scriptset name will be used to search for yourname.sh|tcl|ps1 or names as you specify in yourname.wrapconfig if it exists" \n - append usage "If no template is specified in a .wrapconfig and no -template argument is supplied, it will default to punk-multishell.cmd" \n - if {![string length $filepath_or_scriptset]} { - puts stderr "No filepath_or_scriptset specified" - puts stderr $usage - return false + set ftail [file rootname [file tail $fname]] ;#e.g example_wrap.toml + set scriptset [lindex [split $ftail _] 0] + set fallback_outputfile $scriptset.cmd + set fallback_nextshellpath "/usr/bin/env tclsh" + set fallback_nextshelltype "tcl" + + if {[tomlish::dict::path::exists $tomldict {.application.default_outputfile.value}]} { + dict set resultd default_outputfile [tomlish::dict::path::get $tomldict {.application.default_outputfile.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshellpath.value}]} { + dict set resultd default_nextshellpath [tomlish::dict::path::get $tomldict {.application.default_nextshellpath.value}] + } + if {[tomlish::dict::path::exists $tomldict {.application.default_nextshelltype.value}]} { + dict set resultd default_nextshelltype [tomlish::dict::path::get $tomldict {.application.default_nextshelltype.value}] + } + foreach platform {win32 dragonflybsd freebsd netbsd linux macosx other} { + set d [dict create] + foreach field {outputfile nextshellpath nextshelltype} { + if {[tomlish::dict::path::exists $tomldict ".application.$platform.$field.value"]} { + dict set d $field [tomlish::dict::path::get $tomldict ".application.$platform.$field.value"] + } else { + if {[dict exists $resultd default_$field]} { + dict set d $field [dict get $resultd default_$field] + } else { + dict set d $field [set fallback_$field] + } + } + } + dict set resultd $platform $d } + + return $resultd + } + punk::args::define { + @id -id ::punk::mix::commandset::scriptwrap::multishell + @cmd -name punk::mix::commandset::scriptwrap::multishell\ + -summary\ + "Wrap script(s) into a polyglot cross-platform executable script."\ + -help\ + "Create a polyglot executable script that wraps constituent scripts written in + various scripting languages such as perl, tcl, shell script, powershell. + The resulting polyglot file should run cross platform on windows and various + types of unix-like OS. For use on windows the output file should be named with + a .bat or .cmd extension - but the same file with extension removed should also + be capable of running on FreeBSD, Linux etc. + Note that a polyglot script such as this may be somewhat brittle over the long + term with regards to default shells and scripting languages across platforms." + @leaders -min 1 -max 1 + filepath_or_scriptset -type string -minsize 1 -help\ + "Supply the path to a single script file to wrap, or the name of a scriptset. + The scriptset name will be used to search for .sh|.bash|.tcl|.ps1|.pl + or alternatively, names as specified in a configuration file named _wrap.toml + if it exists in the current folder, or is specified with a full path name. + If no template name/path is specified in a _wrap.toml file and no + -template argument is supplied the default punk.multishell.cmd will be used. + If the template is specified explicitly in -template as well as in the .toml + file - the supplied -template argument will override that specified in the + .toml file." + @opts + -template -type string -default "punk.multishell.cmd" -help\ + "Templates are provided from modules or paths in the current project, + so available templates will vary based on whether the multishell + command is being run from within a project directory or not. + To see available templates use punk::mix::commandset::scriptwrap::templates." + -outputfolder -type directory -default "" -help\ + "Folder to which to write resulting polyglot script. + If empty, the output will go to the /bin folder or + to the current working directory if there is no projectroot." + -askme -type boolean -default 1 -help\ + "Prompt user at console (stdin) for confirmation of operations such as + overwrite." + -force -type boolean -default 0 + -returnextra -type boolean -default 0 + @values -minvalues 0 -maxvalues 0 + } + #: + #@SET "nextshellpath[win32___________]=tclsh___________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshelltype[other___________]=tcl_____________" + #: + proc multishell {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] + lassign [dict values $argd] leaders opts values received + # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_askme [dict get $opts -askme] - set opt_template [dict get $opts -template] - set opt_outputfolder [dict get $opts -outputfolder] - set opt_returnextra [dict get $opts -returnextra] - set opt_force [dict get $opts -force] + set filepath_or_scriptset [dict get $leaders filepath_or_scriptset] + set opt_askme [dict get $opts -askme] + set opt_template [dict get $opts -template] ;#use dict exists $received -template to see if overridable in .toml + set opt_outputfolder [dict get $opts -outputfolder] + set opt_returnextra [dict get $opts -returnextra] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- - set ext [file extension $filepath_or_scriptset] set startdir [pwd] + set allowed_extensions [list tcl ps1 sh bash pl] + #TODO - distinct sections for sh vs bash? needs experiments.. + #for now we use shell-pre-launch-subprocess etc + #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] + set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] + + if {[file pathtype $filepath_or_scriptset] ni {absolute relative}} { + error "bad pathtype for '$filepath_or_scriptset' (expected absolute or relative path, or name of scriptset)" + } - - - #first check if relative or absolute path matches a file + #first check if absolute path matches a file or relative path from cwd matches a file if {[file pathtype $filepath_or_scriptset] eq "absolute"} { - set specified_path $filepath_or_scriptset + set specified_path $filepath_or_scriptset } else { set specified_path [file join $startdir $filepath_or_scriptset] } + set scriptdir [file dirname $specified_path] + set ext [string trim [file extension $filepath_or_scriptset] .] - set allowed_extensions [list wrapconfig tcl ps1 sh bash pl] - set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] - #set allowed_extensions [list tcl] - set found_script 0 - if {[file exists $specified_path]} { - set found_script 1 + set scriptset "" + if {$ext eq ""} { + set scriptset [file rootname [file tail $specified_path]] + } elseif {$ext eq "toml"} { + set tomltail [file tail $specified_path] + if {[string match *_wrap.toml $tomltail]} { + set scriptset [lindex [split $tomltail _] 0] + #if .toml was specified - the config file must exist + if {![file exists $specified_path]} { + if {[file pathtype $filepath_or_scriptset] eq "relative"} { + puts stderr "unable to locate '$specified_path' - will continue search in src/scriptapps folder" + } else { + #caller was specific about path - no fallback to src/scriptapps + error "unable to locate '$specified_path'" + } + } + } else { + error "supplied toml file must be of form _wrap.toml" + } } else { - foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { - if {[file exists $filepath_or_scriptset.$e]} { - set found_script 1 - break + if {$ext ni $allowed_extensions} { + error "supplied filepath_or_scriptset must be the name of a scriptset without extension, a file named _wrap.toml, or a script with one of the extensions: $allowed_extensions" + } + } + + set list_input_files [list] + set configd [dict create] + if {$scriptset ne ""} { + puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" + #.toml file may or may not exist + if {[file exists ${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptdir $s] + } + } + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" + return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptdir" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptdir/$scriptset.$e]} { + lappend list_input_files $scriptdir/$scriptset.$e + } } } + } else { + #expect a single script + if {[file exists $specified_path]} { + lappend list_input_files $specified_path + } } + set found_script [expr {[llength $list_input_files] > 0}] #TODO! - use get_wrapper_folders - multishell should use same available templates as the 'templates' function - set scriptset [file rootname [file tail $specified_path]] if {$found_script} { - if {[file type $specified_path] eq "file"} { - set specified_root [file dirname $specified_path] - set pathinfo [punk::repo::find_repos [file dirname $specified_path]] - set projectroot [dict get $pathinfo closest] + #found scripts at absolute path - or path relative to cwd + set scriptroot $scriptdir + set pathinfo [punk::repo::find_repos $scriptroot] + set projectroot [dict get $pathinfo closest] + if {[file exists $scriptroot/wrappers]} { + set customwrapper_folder $scriptroot/wrappers + } else { + #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder if {[string length $projectroot]} { - #use the specified files folder - but use the main scriptapps/wrappers folder if specified one has no wrappers subfolder - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - set customwrapper_folder $projectroot/src/scriptapps/wrappers - } + set customwrapper_folder $projectroot/src/scriptapps/wrappers } else { #outside of any project - set scriptroot [file dirname $specified_path] - if {[file exists $scriptroot/wrappers]} { - set customwrapper_folder $scriptroot/wrappers - } else { - #no customwrapper folder available - set customwrapper_folder "" - } + set customwrapper_folder "" } - } else { - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false } } else { + if {[file pathtype $filepath_or_scriptset] eq "absolute"} { + return false + } set pathinfo [punk::repo::find_repos $startdir] set projectroot [dict get $pathinfo closest] - if {[string length $projectroot]} { - if {[llength [file split $filepath_or_scriptset]] > 1} { - puts stderr "filepath_or_scriptset looks like a path - but doesn't seem to point to a file" - puts stderr "Ensure you are within a project and use just the name of the scriptset, or pass in the full correct path or relative path to current directory" - puts stderr $usage - return false - } else { - #we've already ruled out empty string - so must have a single element representing scriptset - possibly with file extension - set scriptroot $projectroot/src/scriptapps - set customwrapper_folder $projectroot/src/scriptapps/wrappers - #check something matches the scriptset.. - set something_found "" - if {[file exists $scriptroot/$scriptset]} { - set found_script 1 - set something_found $scriptroot/$scriptset ;#extensionless file - that's ok too - } else { - foreach e $allowed_extensions { - if {[file exists $scriptroot/$scriptset.$e]} { - set found_script 1 - set something_found $scriptroot/$scriptset.$e - break - } + if {![string length $projectroot]} { + puts stderr "No matching scripts or config found for $filepath_or_scriptset, and you are not within a directory where projectroot and src/scriptapps can be determined" + return false + } + + set scriptroot $projectroot/src/scriptapps + set customwrapper_folder $projectroot/src/scriptapps/wrappers + #check something matches the scriptset.. + if {$scriptset ne ""} { + #.toml file may or may not exist + if {[file exists $scriptroot/${scriptset}_wrap.toml]} { + puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + if {[dict exists $configd scripts]} { + set configured_scripts [dict get $configd scripts] + foreach s $configured_scripts { + lappend list_input_files [file join $scriptroot $s] } } - if {!$found_script} { - puts stderr "Searched within $scriptroot" - puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" - puts stderr $usage + if {![llength $list_input_files]} { + puts stderr "No input script files defined in {$scriptset}_wrap.toml" return false - } else { - if {[file type $something_found] ne "file"} { - puts stderr "Found '$something_found'" - puts stderr "wrap_in_multishell doesn't currently support a directory as the path." - puts stderr $usage - return false + } + } else { + puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" + puts stdout "Will look for the following scripts in $scriptroot" + foreach e $allowed_extensions { + puts stderr "$scriptset.$e" + } + foreach e [concat $allowed_extensions [string toupper $allowed_extensions]] { + if {[file exists $scriptroot/$scriptset.$e]} { + lappend list_input_files $scriptroot/$scriptset.$e } } } - } else { - puts stderr "filepath_or_scriptset parameter doesn't seem to refer to a file, and you are not within a directory where projectroot and src/scriptapps/wrappers can be determined" - puts stderr $usage - return false + #expect a single script + if {[file exists $scriptroot/$filepath_or_scriptset]} { + if {[file type $scriptroot/$filepath_or_scriptset] ne "file"} { + puts stderr "wrap_in_multishell doesn't currently support a directory as the path. path: $scriptroot/$filepath_or_scriptset" + return false + } + lappend list_input_files $scriptroot/$filepath_or_scriptset + } } - } - #assertion - customwrapper_folder var exists - but might be empty + set found_script [expr {[llength $list_input_files] > 0}] - - if {[string length $ext]} { - #If there was an explicitly supplied extension - then that file should exist - if {![file exists $scriptroot/$scriptset.$ext]} { - puts stderr "Explicit extension .$ext was supplied - but matching file not found." - puts stderr $usage - return false - } else { - if {$ext eq "wrapconfig"} { - set process_extensions ALLFOUNDORCONFIGURED + #---------------------- + if {!$found_script} { + puts stderr "Searched within $scriptdir and $scriptroot" + if {$scriptset ne ""} { + puts stderr "Unable to find a file matching $scriptset or one of the extensions: $allowed_extensions" } else { - set process_extensions $ext + puts stderr "Unable to find file $filepath_or_scriptset" } + return false } - } else { - #no explicit extension - process all for scriptset - set process_extensions ALLFOUNDORCONFIGURED + } - #process_extensions - either a single one - or all found or as per .wrapconfig + #assertion - customwrapper_folder var exists - but might be empty - if {$opt_template eq "\uFFFF"} { - set templatename punk.multishell.cmd + if {[dict exists $configd template]} { + set templatename [dict get $configd template] } else { - set templatename $opt_template + if {$opt_template eq "\uFFFF"} { + set templatename punk.multishell.cmd + } else { + set templatename $opt_template + } } set templatename_root [file rootname [file tail $templatename]] @@ -995,7 +1136,7 @@ namespace eval punk::mix::commandset::scriptwrap { set template_base_dict [punk::mix::base::lib::get_template_basefolders] set tpldirs [list] dict for {tdir tsourceinfo} $template_base_dict { - set vendor [dict get $tsourceinfo vendor] + set vendor [dict get $tsourceinfo vendor] if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { lappend tpldirs $tdir } elseif {[file exists $tdir/utility/scriptappwrappers/${templatename_fileroot}[file extension $templatename]]} { @@ -1032,7 +1173,7 @@ namespace eval punk::mix::commandset::scriptwrap { } - if {$opt_outputfolder eq "\uFFFF"} { + if {$opt_outputfolder eq ""} { #outputfolder not explicitly specified by caller if {[string length $projectroot]} { set output_folder [file join $projectroot/bin] @@ -1056,13 +1197,36 @@ namespace eval punk::mix::commandset::scriptwrap { #todo - #output_file extension may also depend on the template being used.. and/or the .wrapconfig - if {$::tcl_platform(platform) eq "windows"} { - set output_extension cmd + #output_file extension may also depend on the template being used.. and/or the _wrap.toml config + + if {[dict size $configd]} { + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other + } + } + set out [dict get $configd $ptype outputfile] + set output_file [file join $output_folder $out] } else { - set output_extension sh + #no _wrap.toml file available + if {$::tcl_platform(platform) eq "windows"} { + set output_extension .cmd + } else { + set output_extension .sh + } + if {$scriptset ne ""} { + set output_file [file join $output_folder $scriptset$output_extension] + } else { + set infile [lindex $list_input_files 0] + set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] + } } - set output_file [file join $output_folder $scriptset.$output_extension] + + if {[file exists $output_file]} { set fdexisting [open $output_file r] fconfigure $fdexisting -translation binary @@ -1103,13 +1267,10 @@ namespace eval punk::mix::commandset::scriptwrap { #foreach ln $template_lines { #} - set list_input_files [list] - if {$process_extensions eq "ALLFOUNDORCONFIGURED"} { - #todo - look for .wrapconfig or all extensions for the scriptset - puts stderr "Sorry - only single input file supported. Supply a file extension or use a .wrapconfig with a single input file for now - implementation incomplete" + if {[llength $list_input_files] > 1} { + #todo + puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" return false - } else { - lappend list_input_files $scriptroot/$scriptset.$ext } #todo - split template at each etc marker and build a dict of parts @@ -1117,7 +1278,6 @@ namespace eval punk::mix::commandset::scriptwrap { #hack - process one input set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] fconfigure $fdscript -translation binary set script_data [read $fdscript] @@ -1131,7 +1291,8 @@ namespace eval punk::mix::commandset::scriptwrap { } puts stdout "-----------------------------------------------\n" puts stdout "Target for above script data is '$output_file'" - set lang [dict get $extension_langs [string tolower $ext]] + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] puts stdout "Language of script being wrapped is $lang" if {$opt_askme} { set answer [util::askuser "Does this look correct? Y|N"] diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd index 2975975d..9daf7ebf 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd @@ -209,6 +209,8 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' SET task_exitcode=66 @REM boundary padding @REM boundary padding + @REM boundary padding + @REM boundary padding GOTO :exit_multishell ) ) @@ -223,7 +225,9 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" - @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" + @REM Although drive letters on windows are normally upper case wslbash seems to expect lower case drive letters + @CALL :stringToLower %drive ldrive + @SET "result=/mnt/%ldrive:~0,1%%_path:\=/%%name%" @ENDLOCAL & ( @if "%~2" neq "" ( SET "%rtrn%=%result%" @@ -336,7 +340,8 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) ) @EXIT /B - +@REM boundary padding +@REM boundary padding :stringToUpper @SETLOCAL @SET "rtrn=%~2" @@ -354,6 +359,25 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) ) @EXIT /B +:stringToLower +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "retstring=%~1" + @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( + @SET "retstring=!retstring:%%A=%%A!" + ) + @SET "result=!retstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToLower %string% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding :stringTrimTrailingUnderscores @SETLOCAL @SET "rtrn=%~2" @@ -397,6 +421,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' :endlib : \ @REM padding +@REM padding @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell @GOTO :exit_multishell # } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm index 6bd826e2..f8e55b02 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm @@ -444,9 +444,8 @@ tcl::namespace::eval punk::ns { set nspath [string map {:::: ::} $nspath] set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] - if {[lindex $parts end] eq ""} { - - } + #if {[lindex $parts end] eq ""} { + #} return $parts } @@ -531,6 +530,21 @@ tcl::namespace::eval punk::ns { return [regexp [dict get $ns_re_cache $glob] $path] } + #namespace tree without globbing or weird ns consideration + proc nstree_raw {{location ::}} { + if {![string match ::* $location]} { + error "nstree_raw requires a fully qualified namespace" + } + nstree_rawlist $location + } + proc nstree_rawlist {location} { + set nslist [list $location] + foreach ch [::namespace children $location] { + lappend nslist {*}[nstree_rawlist $ch] + } + return $nslist + } + proc nstree {{location ""}} { if {![string match ::* $location]} { set nscaller [uplevel 1 {::namespace current}] @@ -3899,6 +3913,7 @@ tcl::namespace::eval punk::ns { } proc _pkguse_vars {varnames} { + #review - obsolete? while {"pkguse_vars_[incr n]" in $varnames} {} #return [concat $varnames pkguse_vars_$n] return [list {*}$varnames pkguse_vars_$n] @@ -3932,10 +3947,12 @@ tcl::namespace::eval punk::ns { #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically + variable pkguse_package_to_namespace [dict create] proc pkguse {args} { + variable pkguse_package_to_namespace set argd [punk::args::parse $args withid ::punk::ns::pkguse] lassign [dict values $argd] leaders opts values received - puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + #puts stderr "leaders:$leaders opts:$opts values:$values received:$received" set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] if {[dict exists $received script]} { @@ -3967,68 +3984,159 @@ tcl::namespace::eval punk::ns { set ver "";# tcl version? } default { - if {[string match ::* $pkg_or_existing_ns]} { - set pkg_unqualified [string range $pkg_or_existing_ns 2 end] - if {![tcl::namespace::exists $pkg_or_existing_ns]} { - set ver [package require $pkg_unqualified] - } else { - set ver "" - } + #- comparing namespaces_before vs namespaces_after only works if the package was not previously loaded + #we could either go to the somewhat expensive route of steaming up an interp with the same auto_path & tcl::tm::list each time.. + #or cache the result of the namespace we picked for later pkguse calls (pkguse_package_to_namespace dict) + #we are using the cache method - but this also doesn't help for packages previously loaded by normal package require + #our aim is for pkguse to be deterministic in what namespace it finds - even if it doesn't always get the ideal one (e.g cookiejar, see below) + #To determine appropriate namespace for already loaded packages where we have no cache entry - we may still need the helper interp mechanism + #The helper interp could be persistent - but only so long as the auto_path/tcl::tm::list values are in sync + #review. + + #also see img::png img::raw etc + #these don't directly load namespaces or direct commands.. just change behaviour of existing commands? + #but they can load things like tk (ttk namespace) first one creates ::tkimg? + + if {[string match ::* $pkg_or_existing_ns] && [tcl::namespace::exists $pkg_or_existing_ns]} { + #pkguse on an existing full qualified namespace does no package require set ns $pkg_or_existing_ns + set ver "" } else { - set pkg_unqualified $pkg_or_existing_ns - set ver [package require $pkg_unqualified] - set ns ::$pkg_unqualified - } - #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index - set previous_command_count 0 - if {[namespace exists $ns]} { - set previous_command_count [llength [info commands ${ns}::*]] - } + if {[string match ::* $pkg_or_existing_ns]} { + set pkg_unqualified [string range $pkg_or_existing_ns 2 end] + } else { + set pkg_unqualified $pkg_or_existing_ns + } + #foreach equiv of while 1 - just to allow early exit with break + foreach code_block single { + if {[dict exists $pkguse_package_to_namespace $pkg_unqualified]} { + set ns [dict get $pkguse_package_to_namespace $pkg_unqualified] + set ver [package provide $pkg_unqualified] + break + } + if {[package provide $pkg_unqualified] ne ""} { + #package has already been loaded + if {[namespace exists ::$pkg_unqualified]} { + set ns ::$pkg_unqualified + set ver [package provide $pkg_unqualified] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + break + } + #existing package but no matching namespace.. + #- load in throwaway interp and see what cmds/namespaces created + interp create nstest + try { + nstest eval {tcl::tm::remove {*}[tcl::tm::list]} + nstest eval [list tcl::tm::add {*}[lreverse [tcl::tm::list]]] + nstest eval [list set ::auto_path $::auto_path] + nstest eval {package require punk::ns} + set ns "" + if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] + set ns [nstest eval $script] + } else { + puts "couldn't test pkg $pkg_unqualified\n$errMsg" + } + } finally { + interp delete nstest + } - #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands - #for the purposes of pkguse - which most commonly interactive - we want the namespace populated - #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + dict set pkguse_package_to_namespace $pkg_unqualified $ns + set ver [package provide $pkg_unqualified] + break + } - if {!$ns_populated} { - #we will catch-run an auto_index entry if any - #auto_index entry may or may not be prefixed with :: - set keys [list] - #first look for exact pkg_unqualified and ::pkg_unqualified - #leave these at beginning of keys list - if {[array exists ::auto_index($pkg_unqualified)]} { - lappend keys $pkg_unqualified - } - if {[array exists ::auto_index(::$pkg_unqualified)]} { - lappend keys ::$pkg_unqualified - } - #as auto_index is an array - we could get keys in arbitrary order - set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] - lappend keys {*}$matches - set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] - lappend keys {*}$matches - set ns_populated 0 - set i 0 - set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing - set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] - while {!$ns_populated && $i < [llength $keys]} { - #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base - #e.g if we are loading ::x::y - #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc - set k [lindex $keys $i] - set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] - if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { - set auto_source [set ::auto_index($k)] - if {$auto_source ni $already_sourced} { - uplevel 1 $auto_source - lappend already_sourced $auto_source - set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + #pkg not loaded + set namespaces_before [nstree_rawlist ::] ;#approx 1ms for 500 or so namespaces - not cheap but bearable + #some packages don't create their namespace immediately and/or don't populate it with commands and instead put entries in ::auto_index + #gathering prior cmdcount for every ns in system is also a somewhat expensive operation.. review + #we don't know for sure that the namespace for the package require operation actually matches the package name + #e.g tcllib inifile package uses namespace ::ini + #e.g sqlite3 package adds commands to the global namespace + set dict_ns_commandcounts [dict create] + foreach nsb $namespaces_before { + dict set dict_ns_commandcounts $nsb [llength [info commands ${nsb}::*]] + } + + set ver [package require $pkg_unqualified] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + set namespaces_after [nstree_rawlist ::] + + if {[llength $namespaces_after] > [llength $namespaces_before]} { + set namespaces_new [struct::set difference $namespaces_after $namespaces_before] + if {$ns ni $namespaces_new} { + #todo - use shortest result? what if this is a namespace from a required sub package? + #e.g cookiejar loads sqlite3,http,tcl::idna which creates ::sqlite3 etc - but cookiejar just creates an object at ::http::cookiejar + #In this specific case we end up in oo::ObjXXX - but would be better placed in ::http, where the new cookiejar command resides + #review - todo? + set pkgs [package names] + set ns ::$pkg_unqualified ;#fallback - tested for existence below + #find something new - that doesn't match another package name + foreach new $namespaces_new { + if {[lsearch $pkgs [string trimleft $new :]] == -1} { + set ns $new + break + } + } } } - incr i - } + if {[tcl::namespace::exists $ns]} { + #review - only cache if exists? + dict set pkguse_package_to_namespace $pkg_unqualified $ns; + } + set previous_command_count 0 + if {[dict exists $dict_ns_commandcounts $ns]} { + set previous_command_count [dict get $dict_ns_commandcounts $ns] + } + + #also if a sub package was loaded first - then the namespace for the base or lower package may exist but have no commands + #for the purposes of pkguse - which most commonly interactive - we want the namespace populated + #It may still not be *fully* populated because we stop at first source that adds commands - REVIEW + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + + if {!$ns_populated} { + #we will catch-run an auto_index entry if any + #auto_index entry may or may not be prefixed with :: + set keys [list] + #first look for exact pkg_unqualified and ::pkg_unqualified + #leave these at beginning of keys list + if {[array exists ::auto_index($pkg_unqualified)]} { + lappend keys $pkg_unqualified + } + if {[array exists ::auto_index(::$pkg_unqualified)]} { + lappend keys ::$pkg_unqualified + } + #as auto_index is an array - we could get keys in arbitrary order + set matches [lsort [array names ::auto_index ${pkg_unqualified}::*]] + lappend keys {*}$matches + set matches [lsort [array names ::auto_index ::${pkg_unqualified}::*]] + lappend keys {*}$matches + set ns_populated 0 + set i 0 + set already_sourced [list] ;#often multiple triggers for the same source - don't waste time re-sourcing + set ns_depth [llength [punk::ns::nsparts [string trimleft $ns :]]] + while {!$ns_populated && $i < [llength $keys]} { + #todo - skip sourcing deeper entries from a subpkg which may have been loaded earlier than the base + #e.g if we are loading ::x::y + #only source for keys the same depth, or one deeper ie ::x::y, x::y, ::x::y::z not ::x or ::x::y::z::etc + set k [lindex $keys $i] + set k_depth [llength [punk::ns::nsparts [string trimleft $k :]]] + if {$k_depth == $ns_depth || $k_depth == $ns_depth + 1} { + set auto_source [set ::auto_index($k)] + if {$auto_source ni $already_sourced} { + puts stderr "pkguse sourcing auto_index script $auto_source" + uplevel 1 $auto_source + lappend already_sourced $auto_source + set ns_populated [expr {[tcl::namespace::exists $ns] && [llength [info commands ${ns}::*]] > $previous_command_count}] + } + } + incr i + } + + } + + }; # end foreach code_block single - scope for use of 'break' } } diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm index a31e255e..fd84ec8d 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm @@ -3567,7 +3567,6 @@ namespace eval repl { if {[catch { package require punk::args - catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical package require punk::config package require punk::ns #puts stderr "loading natsort" @@ -3589,6 +3588,7 @@ namespace eval repl { }} [punk::config::configure running] package require textblock + catch {package require punk::args::tclcore} ;#while tclcore is highly desirable, and should be installed with punk::args - it's not critical } errM]} { puts stderr "========================" puts stderr "code interp error:" diff --git a/src/vfs/_vfscommon.vfs/modules/textblock-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/textblock-0.1.3.tm index 472edc54..f2f4a3af 100644 --- a/src/vfs/_vfscommon.vfs/modules/textblock-0.1.3.tm +++ b/src/vfs/_vfscommon.vfs/modules/textblock-0.1.3.tm @@ -6007,7 +6007,7 @@ tcl::namespace::eval textblock { proc welcome_test {} { package require punk::ansi - set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] + set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/ROY-WELC.ANS 80x8]] # Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a]