: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported 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, zsh, bash, (sh diversion) 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 ------------------------------------------------------------------------------------------------------------------------------- @rem return from endlocal macro - courtesy of jeb @rem This allows return of values containing special characters from subroutines @rem https://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal/8257951#8257951 @rem ------------------------------------------------------------------------------------------------------------------------------- @setlocal DisableDelayedExpansion @echo off %= 2 blank lines after next are required =% set LF=^ set ^"\n=^^^%LF%%LF%^%LF%%LF%^^" %= I use EDE for EnableDelayeExpansion and DDE for DisableDelayedExpansion =% set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n% setlocal EnableDelayedExpansion%\n% %= Take all variable names into the varName array =%%\n% set varName_count=0%\n% for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n% %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n% %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n =%%\n% %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n% %= The correct line will be detected by the (missing) enclosing quotes =%%\n% set "retContent=1!LF!"%\n% for /L %%n in (0 1 !varName_count!) do (%\n% for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n% set "content=!%%C!"%\n% set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n% if defined content (%\n% %= This complex block is only for replacing '!' with '^!' =%%\n% %= First replacing '"'->'""q' '^'->'^^' =%%\n% set ^"content_EDE=!content:"=""q!"%\n% set "content_EDE=!content_EDE:^=^^!"%\n% %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n% call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n% %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!' =%%\n% set "content_EDE=!content_EDE:""e=^!"%\n% %= Now restore the quotes =%%\n% set ^"content_EDE=!content_EDE:""q="!"%\n% ) ELSE set "content_EDE="%\n% set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n% )%\n% )%\n% %= Now return all variables from retContent over the barrier =%%\n% for /F "delims=" %%V in ("!retContent!") DO (%\n% %= Only the first line can contain a single 1 =%%\n% if "%%V"=="1" (%\n% %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n% endlocal%\n% endlocal%\n% ) ELSE (%\n% %= This is true in EDE =%%\n% if "!"=="" (%\n% if %%V==%%~V (%\n% %%V !%\n% )%\n% ) ELSE IF not %%V==%%~V (%\n% %%~V%\n% )%\n% )%\n% )%\n% ) else set args=" @rem ------------------------------------------------------------------------------------------------------------------------------- @SETLOCAL EnableExtensions EnableDelayedExpansion @REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system @REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder @REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) @REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash 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 @SET "validshelltypes= pwsh____________ powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________ none____________" @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 64 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___________]=cmd.exe /c powershell -nop -nol -ExecutionPolicy bypass -File___________________________________________________________________" @SET "nextshelltype[win32___________]=powershell______" @SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[dragonflybsd____]=bash____________" @SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[freebsd_________]=bash____________" @SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[netbsd__________]=bash____________" @SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[linux___________]=bash____________" @SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[macosx__________]=bash____________" @SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________________________________________________________________________" @SET "nextshelltype[other___________]=bash____________" : <> @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" : <> @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 filepath @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" %= e.g c:\punkshell\bin\ %= @SET "fname=%~nx0" @SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =% @SET "fullscriptname=%~dp0%~n0%~x0" @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% @goto skip_parameter_wrangling @set argCount=30 @rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe @rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon @rem Set argCount higher if desired, but there is a small amount of additional looping overhead. @set tmpfile_base=%TEMP%\punkbatch_params @call :getUniqueFile %tmpfile_base% ".txt" paramfile @echo %paramfile% %= NOTE when we loop like this using the percent-n args and shift, we lose unquoted separators such as comma and semicolon %= @rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124 @rem outer loop required to redirect all rem lines at once to file @for %%x in (1) do @( @for /L %%f in (1,1,%argCount%) do @( @set "argnum=%%~nf" @set "a1=%%1" @rem @set "argname=%%!argnum!" @rem @echo argname: !argname! @call :rem_output !argnum! !a1! @shift ) ) > %paramfile% @echo off @set "newcommandline= " @(set target=cmd_pwsh) @if "%target%"=="cmd_pwsh" ( @for /F "delims=" %%L in (%paramfile%) do @( SETLOCAL DisableDelayedExpansion set "param=%%L" @REM @echo ######### %%L @rem call :buildcmdline newcommandline param "{" "}" @rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %= call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %= @rem @echo . ) ) ELSE ( @for /F "delims=" %%L in (%paramfile%) do @( SETLOCAL DisableDelayedExpansion set "param=%%L" call :buildcmdline newcommandline param ) ) @REM padding SETLOCAL EnableDelayedExpansion @echo off @IF EXIST %paramfile% ( @DEL /F /Q %paramfile% ) @IF EXIST %paramfile% ( echo failed to delete %paramfile% cat %paramfile% ) :skip_parameter_wrangling @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 @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 pre = "/c %fullscriptname% PUNK-ELEVATED " >> "%vbsGetPrivileges%" @REM @echo pre = "PUNK-ELEVATED " >> "%vbsGetPrivileges%" @echo args = pre >> "%vbsGetPrivileges%" @ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%" @ECHO args = args ^& Chr(34) ^& strArg ^& Chr(34) ^& " " >> "%vbsGetPrivileges%" @ECHO Next >> "%vbsGetPrivileges%" @GOTO skiptest %= Option Explicit =% %= We need a child process to locate the current script. =% @ECHO Const FLAG_PROCESS = "winver.exe" >> "%vbsGetPrivileges%" %= ' WMI constants %= @ECHO Const wbemFlagForwardOnly = 32 >> "%vbsGetPrivileges%" %=' Generate a unique value to be used as a flag =% @ECHO Dim guid >> "%vbsGetPrivileges% @ECHO guid = Left(CreateObject("Scriptlet.TypeLib").GUID,38) >> "%vbsGetPrivileges%" %= ' Start a process using the indicated flag inside its command line =% @ECHO WScript.CreateObject("WScript.Shell").Run """" ^& FLAG_PROCESS ^& """ " ^& guid, 0, False >> "%vbsGetPrivileges%" %= ' To retrieve process information a WMI reference is needed =% @ECHO Dim wmi >> "%vbsGetPrivileges%" @ECHO Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}^!\\.\root\cimv2") >> "%vbsGetPrivileges%" %= ' Query the list of processes with the flag in its command line, retrieve the =% %= ' process ID of its parent process ( our script! ) and terminate the process =% @ECHO Dim colProcess, process, myProcessID >> "%vbsGetPrivileges%" @ECHO Set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%" @ECHO "SELECT ParentProcessID From Win32_Process " ^& _>> "%vbsGetPrivileges%" @ECHO "WHERE Name='" ^& FLAG_PROCESS ^& "' " ^& _>> "%vbsGetPrivileges%" @ECHO "AND CommandLine LIKE '%%" ^& guid ^& "%%'" _>> "%vbsGetPrivileges%" @ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%" @ECHO ) >> "%vbsGetPrivileges%" @ECHO For Each process In colProcess >> "%vbsGetPrivileges%" @ECHO myProcessID = process.ParentProcessID >> "%vbsGetPrivileges%" @ECHO process.Terminate >> "%vbsGetPrivileges%" @ECHO Next >> "%vbsGetPrivileges%" %= ' Knowing the process id of our script we can query the process list =% %= ' and retrieve its command line =% @ECHO Dim commandLine >> "%vbsGetPrivileges%" @ECHO set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%" @ECHO "SELECT CommandLine From Win32_Process " ^& _>> "%vbsGetPrivileges%" @ECHO "WHERE ProcessID=" ^& myProcessID _>> "%vbsGetPrivileges%" @ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%" @ECHO ) >> "%vbsGetPrivileges%" @ECHO For Each process In colProcess >> "%vbsGetPrivileges%" @ECHO commandLine = process.CommandLine >> "%vbsGetPrivileges%" @ECHO Next >> "%vbsGetPrivileges%" @ECHO WScript.Echo "raw commandline: " ^& commandLine >>"%vbsGetPrivileges%" %= ' Done =% @ECHO intpos = 0 >> "%vbsGetPrivileges%" @ECHO intCount = 0 >> "%vbsGetPrivileges%" @ECHO intstartsearch = 1 >> "%vbsGetPrivileges%" @ECHO intmax = 100 >> "%vbsGetPrivileges%" @ECHO do While intCount ^< 4 and intmax ^> 0 >> "%vbsGetPrivileges%" @ECHO intpos = InStr(intstartsearch, commandline, """") >> "%vbsGetPrivileges%" @ECHO if intpos ^<^> 0 then >> "%vbsGetPrivileges%" @ECHO intCount = intCount + 1 >> "%vbsGetPrivileges%" @ECHO if intcount = 4 then >> "%vbsGetPrivileges%" @ECHO ' wscript.echo "position: " ^& intpos >> "%vbsGetPrivileges%" @ECHO commandline = Mid(commandline,intpos+1) >> "%vbsGetPrivileges%" @ECHO exit do >> "%vbsGetPrivileges%" @ECHO else >> "%vbsGetPrivileges%" @ECHO intstartsearch = intpos + 1 >> "%vbsGetPrivileges%" @ECHO end if >> "%vbsGetPrivileges%" @ECHO end if >> "%vbsGetPrivileges%" @ECHO intmax = intmax -1 >> "%vbsGetPrivileges%" @ECHO Loop >> "%vbsGetPrivileges%" @ECHO if intcount ^< 4 then >> "%vbsGetPrivileges%" @ECHO err.raise vbObjectError + 1001, "vbsGetPrivileges", "failed to parse commandline" >> "%vbsGetPrivileges%" @ECHO end if >> "%vbsGetPrivileges%" @ECHO commandline = pre ^& commandline >> "%vbsGetPrivileges%" @ECHO WScript.Echo "commandline: " ^& commandLine >>"%vbsGetPrivileges%" @ECHO WScript.Echo "args: " ^& args >>"%vbsGetPrivileges%" :skiptest @ECHO UAC.ShellExecute "cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%" @REM @ECHO UAC.ShellExecute "%fullscriptname%", commandline, "", "runas", 1 >> "%vbsGetPrivileges%" @ECHO Launching script "%fullscriptname%" in new window due to administrator elevation with args: "%*" @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @REM @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" !newcommandline! @EXIT /B @REM buffer @REM buffer :gotPrivileges @REM setlocal & pushd . @PUSHD . @cd /d %winpath% @IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( @DEL "%vbsGetPrivileges%" 1>nul 2>nul @SET arglist=%arglist:~14% @SHIFT ) :skip_privileges @SET need_ps1=0 @REM we want the ps1 to exist even if the nextshell isn't powershell @if not exist "%scriptrootname%.ps1" ( @SET need_ps1=1 ) ELSE ( fc "%fullscriptname%" "%scriptrootname%.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 "%fullscriptname%" "%scriptrootname%.ps1" >NUL ) @REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /? @IF "!selected_shelltype_trimmed!"=="none" ( SET selected_shelltype_trimmed=pwsh ) @REM @SET "squoted_args=" @REM @for %%a in (%*) do @( @REM set "v=%%a" @REM set "v=!v:'=''!" @REM SET "squoted_args=!squoted_args!'!v!' " @REM ) @REM @SET "squoted_args=%squoted_args:~0,-1%" @REM @ECHO %squoted_args% @IF "!selected_shelltype_trimmed!"=="pwsh" ( REM pwsh 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 REM when run without cmd.exe - pwsh will receive the semicolon (for cmd.exe unquoted semicolon and comma are separators that aren't seen in positional arguments) pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted 2>NUL; 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 ( @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist% @rem pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" %arglist% %= ok =% @rem cmd /c pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline! !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist% SET task_exitcode=!errorlevel! ) ELSE ( REM TODO prompt user with option to call script to install pwsh using winget %= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =% cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist% @rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline! SET task_exitcode=!errorlevel! ) ) ELSE ( IF "!selected_shelltype_trimmed!"=="powershell" ( %= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =% @rem @echo powershell - !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist% @rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist% %= ok - this works =% !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist% @rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline! 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 @REM @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!" !selected_shellpath_trimmed! "%winpath%%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 batch file library functions @GOTO :endlib @REM padding xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @REM padding xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx %= ---------------------------------------------------------------------- =% @rem courtesy of dbenham :: Example usage @rem call :getUniqueFile "d:\test\myFile" ".txt" myFile @rem echo myFile="%myFile%" @rem 2025 - wmic deprecated :/ @rem 2025 - output of 'wmic os get localDateTime' was something like: @rem LocalDateTime @rem 20251015234316.777000+660 @rem !time! has a resolution of centiseconds. As we test in a loop for file existence, that should be ok. :getUniqueFile baseName extension rtnVar setlocal enabledelayedexpansion :getUniqueFileLoop set "r=!date!_!time!" set "r=%r::=.%" set "r=%r: =%" set "rtn=%~1_!r!%~2" echo "### %rtn%" if exist "%rtn%" ( goto :getUniqueFileLoop ) else ( 2>nul >nul (9>"%rtn%" timeout /nobreak 1) || goto :getUniqueFileLoop ) endlocal & set "%~3=%rtn%" exit /b %= ---------------------------------------------------------------------- =% @REM padding :buildcmdline cmdlinevar paramvar wrapA wrapB %= quoting for cmd.exe /c pwsh -nop !args! =% @SETLOCAL EnableDelayedExpansion @REM @echo ===================== set "pval=!%~2:*#=!" set "pval=!pval:~0,-2!" @REM set "pval=!pval:~0,-1!" set "wrapa=%~3" set "wrapb=%~4" @call :strlen pval slen @rem @echo strlen: !slen! if "!slen!"=="0" ( goto :eof ) @set /A n = !slen! - 1 @(set str=) @set "dq="" @set "bang=^!" @(set carat=^^) @for /l %%i in (0,1,!n!) do @( (set c=!pval:~%%i,1!) if "!c!"=="|" ( set "ch=^^!pval:~%%i,1!" ) ELSE IF "!c!"=="(" ( set "ch=^(" ) ELSE if "!c!"==")" ( set "ch=^)" ) ELSE if "!c!"=="&" ( set "ch=^^&" ) ELSE if "!c!"=="!dq!" ( set "ch=^"" ) ELSE if "!c!"=="!bang!" ( @rem - double caret - first for initial parsing, second to ensure remains escaped during delayed expansion phase @rem - REVIEW set "ch=^^!bang!" ) ELSE if "!c!"=="^carat" ( set "ch=^^^^" ) ELSE if "!c!"=="'" ( set "ch=''" ) ELSE ( set "ch=!c!" ) @rem @echo - !ch! set "str=!str!!ch!" ) echo +++++ %~1 = !%1! !str! set "%~1=!%1! !wrapa!!str!!wrapb!" @rem old method of return - failes to preserve exclamation marks @rem for /f "delims=" %%A in (""!str!"") do endlocal & set "%~1=!%1! '%%~A'" @rem macro method of endlocal return - preserving !val! @echo off %endlocal% %~1 @exit /b :rem_output @rem --------------------------------------------- @rem rem_output is called for each n in the number of args we process - as we don't have a non-destructive way to count args whilst accepting special chars @rem we therefore need a way to stop processing on the last received arg so we don't write argCount entries to param.txt if less are received @rem see 'disappearing quotes' technique @rem https://stackoverflow.com/questions/4643376/how-to-split-double-quoted-line-into-multiple-lines-in-windows-batch-file/4645113#4645113 @rem and @rem https://groups.google.com/g/alt.msdos.batch.nt/c/J71F17Bve9Y (sponge belly) @echo off setlocal enableextensions disabledelayedexpansion set "param1=%~2" rem do must not be indented for %%^" in ("") ^ do if not defined param1 set %%~"param1=%2%%~" if not defined param1 goto :eof endlocal @rem --------------------------------------------- @PROMPT @ @echo on rem %1 #%2# @exit /b @rem padding @REM courtesy of: https://stackoverflow.com/users/463115/jeb :strlen stringVar returnVar @( setlocal EnableDelayedExpansion @SET "rtrn=%~2" (set^ tmp=!%~1!) @rem @echo jjjjj !tmp! @if defined tmp ( set "len=1" @for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do @( @if "!tmp:~%%P,1!" NEQ "" ( set /a "len+=%%P" set "tmp=!tmp:~%%P!" ) ) ) ELSE ( set len=0 ) ) @( endlocal @IF "%~2" neq "" ( @SET "%rtrn%=%len%" ) ELSE ( @ECHO :strlen result: %len% ) exit /b ) :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 @REM padding @REM padding :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 @REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @SET "rtrn=%~1" @ENDLOCAL & ( @IF "%~1" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding @REM boundary padding :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' @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 @REM boundary padding @REM boundary padding @REM boundary padding :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 strvar returnvar @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 @REM boundary padding @REM boundary padding @REM boundary padding @REM boundary padding :stringTrimTrailingUnderscores @SETLOCAL @SET "rtrn=%~2" @SET "string=%~1" @SET "trimstring=%~1" @REM trim up to 127 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 "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 # -- It is tuned to run (and possibly divert to different payload shell) when called from cmd.exe as a batch file, tclsh,sh,zsh,bash,perl or pwsh/powershell script # -- i.e it is a polyglot file. # -- The payload target (by os) is defined in the nextshell block at the top which is constructed when generating the polyglot # -- using the tcl 'dev scriptwrap.multishell' command in a tcl punk shell # -- The payload can be tcl,perl,powershell/pwsh or zsh/bash. # -- 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 ./scriptname.cmd in sh or zsh or bash # -- e.g tclsh scriptname.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 '@ #--------------------------------------------------------------------- puts "info script : [info script]" #puts "argcount : $::argc" #puts "argvalues: $::argv" #puts "argv0 : $::argv0" # -- --- --- --- --- --- --- --- --- --- --- --- #divert to configured nextshell set script_as_called [info script] package require platform set plat_full [platform::generic] set plat [lindex [split $plat_full -] 0] #may be old tcl - not assuming readFile available set fd [open [info script] r] set scriptdata [read $fd] close $fd set scriptdata [string map [list \r\n \n] $scriptdata] set in_data 0 set nextshellpath "" set nextshelltype "" puts stderr "PLAT: $plat" switch -glob -- $plat { "msys" - "mingw*" { set os "win32" } default { set os $plat } } foreach ln [split $scriptdata \n] { if {[string trim $ln] eq ""} {continue} if {!$in_data} { if {[string match ": <>*" $ln]} { set in_data 1 } } else { if {[string match "*@SET*nextshellpath?${os}_*" $ln]} { set lineparts [split $ln =] set tail [lindex $lineparts 1] set nextshellpath [string trimright $tail {_"}] if {$nextshellpath ne "" && $nextshelltype ne ""} { break } } elseif {[string match "*@SET*nextshelltype?${os}_*" $ln]} { set lineparts [split $ln =] set tail [lindex $lineparts 1] set nextshelltype [string trimright $tail {_"}] if {$nextshellpath ne "" && $nextshelltype ne ""} { break } } elseif {[string match ": <>*" $ln]} { break } } } if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} { set script_rootname [file rootname $script_as_called] if {$nextshelltype in "pwsh powershell"} { # experimental set script_ps1 $script_rootname.ps1 set arglist $::argv if {[file extension $script_as_called] ne ".ps1"} { #we need to ensure .ps1 is up to date set needs_updating 0 if {![file exists $script_ps1]} { set needs_updating 1 } else { #both exist if {[file size $script_as_called] != [file size $script_ps1]} { set needs_updating 1 } else { #both exist with same size - do full check that they're identical catch {package require sha256} if {[package provide sha256] ne ""} { set h1 [sha2::sha256 -hex -file $script_as_called] set h2 [sha2::sha256 -hex -file $script_ps1] if {[string length $h1] != 64 || [string length $h2] != 64} { set needs_updating 1 } elseif {$h1 ne $h2} { set needs_updating 1 } } else { #manually compare - scripts aren't too big, so slurp and string compare is fine set fd [open $script_as_called] chan configure $fd -translation binary set data1 [read $fd] close $fd set fd [open $script_ps1] chan configure $fd -translation binary set data2 [read $fd] close $fd if {![string equal $data1 $data2]} { set needs_updating 1 } } } } if {$needs_updating} { file copy -force $script_as_called $script_ps1 } } else { #when called on the .ps1 - we assume it's up to date - review } set scrname $script_ps1 #set arglist [list] #foreach a $::argv { # set a "'$a'" # lappend arglist $a #} } else { set scrname $script_as_called set arglist $::argv } #todo - handle /usr/bin/env #todo - exitcode #review - test spaced quoted words in nextshellpath? # #if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} { # set nextshell_words [list $nextshellpath] #} else { # set nextshell_words $nextshellpath #} #perform any msys argument munging on a cmd/cmd.exe based nextshellpath before we convert the first word to an auto_exec path switch -glob -- $plat { "msys" - "mingw*" { set cmdword [lindex $nextshellpath 0] #we only act on cmd or cmd.exe - not a full path such as c:/WINDOWS/system32/cmd.exe #the nextshellpath should generally be configured as cmd /c ... or cmd.exe ... but specifying it as a path could allow bypassing this un-munging. #The un-munging only applies to msys/mingw, so such bypassing should be unnecessary - review #maint: keep this munging in sync with zsh/bash and perl blocks which must also do msys mangling if {[regexp {^cmd$|^cmd[.]exe$} $cmdword]} { #need to deal with msys argument munging puts stderr "cmd call via msys detected. performing translation of /c to //C" #for now we only deal with /C or /c - todo - other cmd.exe flags? #In this context we would usually only be using cmd.exe /c to launch older 'desktop' powershell to avoid spaced-argument problems - so we aren't expecting other flags set new_nextshellpath [list $cmdword] #for now - just do what zsh munging does - bash regex/string/array processing is tedious and footgunny for the unfamiliar (me), #so determine the minimum viable case for code there, then port behaviour to perl/tcl msys munging sections. foreach w [lrange $nextshellpath 1 end] { if {[regexp {^/[Cc]$} $w]} { lappend new_nextshellpath {//C} } else { lappend new_nextshellpath $w } } set nextshellpath $new_nextshellpath } } } set ns_firstword [lindex $nextshellpath 0] #review - is this test for extra layer of double quoting on first word really necessary? #if we are treaing $nextshellpath as a tcl list - the first layer of double quotes will already have disappeared ##if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} { ## set ns_firstword [string range $ns_firstword 1 end-1] ##} if {$::tcl_platform(platform) ne "windows" && [string match {/*/env} $ns_firstword]} { set exec_part $nextshellpath } else { set epath [auto_execok $ns_firstword] if {$epath eq ""} { error "unable to find executable path for first word '$ns_firstword' of nextshellpath '$nextshellpath'" } else { set exec_part [list {*}$epath {*}[lrange $nextshellpath 1 end]] } } puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist" puts stdout "exec: $exec_part $scrname $arglist" catch {exec {*}$exec_part $scrname {*}$arglist <@stdin >@stdout 2>@stderr} emsg eopts if {[dict exists $eopts -errorcode]} { set ecode [dict get $eopts -errorcode] if {[lindex $ecode 0] eq "CHILDSTATUS"} { exit [lindex $ecode 2] } else { puts stderr "error calling next shell $nextshelltype :" puts stderr $emsg exit 1 } } else { exit 0 } } #--------------------------------------------------------------------- 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 stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # # -- --- --- --- --- --- --- --- --- --- --- --- # -- 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 # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" # use oldschool backticks and sed (posix - lowest common denominator) \ # ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` \ # some ps impls will return arguments - so last field not always appropriate \ # some ps impls don't have -o (e.g cygwin) so ps_shellname may remain empty and emit an error \ ps_shellname=`ps -o pid,comm -p $$ | awk '$1 != "PID" {print $2}'` # \ echo "shell from ps: $ps_shellname" # \ echo "args: $@" # ------------------------------------------------------------------------------ # -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. if false==false # else { then : # # https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) # https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix # posix compliant pop pop() { __pop_n=$(($1 - ${2:-1})) if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then POP_EXPR='set -- "${@:1:'$__pop_n'}"' elif [ $__pop_n -ge 500 ]; then POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" else __pop_index=0 __pop_arguments="" while [ $__pop_index -lt $__pop_n ]; do __pop_index=$((__pop_index+1)) __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" done POP_EXPR="set -- $__pop_arguments" fi } # ------------------------------------------------------------------------------ # non-bash-like posix diversion # we don't use $BASH_VERSION/$ZSH_VERSION as these can still be set when for example # sh is a symlink to bash (posix-mode bash - reduced bashism capabilities?) # if our ps_shellname didn't contain a result, don't divert and risk looping if [ -n "$ps_shellname" ] && [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ] ; then shift pop $# eval "$POP_EXPR" echo "divert to bash $0 $@" /usr/bin/env bash "$0" "$@" exit $? fi # close false==false block fi # close tcl wrap } # ------------------------------------------------------ # -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # # zsh/bash \ shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- zsh/bash script section # -- # -- review - for zsh do we want to use: setopt KSH_ARRAYS ? # -- arrays in bash 0-based vs 1-based in zsh # -- stick to the @:i:len syntax which is same for both # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" elif [[ "$plat" == "FreeBSD"* ]]; then os="freebsd" elif [[ "$plat" == "DragonFly"* ]]; then os="dragonflybsd" elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" elif [[ "$plat" == "MINGW32"* ]]; then os="win32" elif [[ "$plat" == "MINGW64"* ]]; then os="win32" elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. #Need to consider the difference between when msys2 was launched (which strips some paths and sets up the environment) # vs if the msys2 sh was called - (actually bash) in which case paths will be different #wsl and cygwin or msys2 can commonly be problematic combinations - primarily due to path issues #e.g "c:/windows/system32/" is quite likely in the path ahead of msys,git etc. #e.g It means a /usr/bin/env bash call may launch the (linux elf) bash for wsl rather than the msys bash # #msys provides win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix' #bash reports $OSTYPE msys #there are statements around the web that cmd /c .. will work under msys2 # - but from experience, it can be required to use cmd //c ... # or MSYS2_ARG_CONV_ECL='*' cmd /c .. # This seems to be because process arguments that look like unix paths are converted to windows paths :/ #review! os="win32" #review - need ps/sed/awk to determine shell? interp=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` #use 'command -v' (shell builtin preferred over external which) shellpath=`command -v $interp` shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname export PATH="$shellfolder${PATH:+:${PATH}}" elif [[ "$OSTYPE" == "win32" ]]; then os="win32" else #os="$OSTYPE" os="other" fi echo ostype: $OSTYPE ## This is the sort of sed that will not work across implementations ## shellconfiglines=$( sed -n "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) #awk tested on linux & freebsd shellconfiglines=$( awk '/^:.*<>.*$/,/^:.*<>.*$/' "$0" | grep $os) # echo $shellconfiglines; # readarray requires bash 4.0 if [[ "$ps_shellname" == "bash" ]]; then readarray -t arr_oslines <<<"$shellconfiglines" elif [[ "$ps_shellname" == "zsh" ]]; then arr_oslines=("${(f)shellconfiglines}") else #fallback - doesn't seem to work in zsh - untested in early bash IFS=$'\n' arr_oslines=($shellconfiglines) IFS=$' \t\n' # review fi nextshellpath="" nextshelltype="" for ln in "${arr_oslines[@]}"; do # echo "---- $ln" if [[ "$ln" == *"nextshellpath"* ]]; then splitln="${ln#*=}" #remove everything through the first '=' pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then echo zsh/bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" script="$0" if [[ "$nextshelltype" == "pwsh" || "$nextshelltype" == "powershell" ]]; then #powershell requires the file extension to be .ps1 (always - on windows) #on other platforms it's not required if a shebang line is used - but this script must be shebangless for portability and to maintain polyglot capabilities. cmdpattern="[.]cmd$" if [[ "$script" =~ $cmdpattern ]]; then ps1script="${script%????}.ps1" if ! cmp -s "$script" "$ps1script" ; then #ps1script either different or missing #on windows - batch script copies .cmd -> .ps1 if not identical cp -f "$script" "$ps1script" fi script=$ps1script fi fi if [[ "$plat" == "MSYS_NT"* ]]; then #we need to deal with MSYS argument munging cmdpattern="^cmd.exe |^cmd " #do not double quote cmdpattern - or it will be treated as literal string if [[ "$nextshellpath" =~ $cmdpattern ]]; then #for now - tell the user what's going on echo "cmd call via msys detected. performing translation of /c to //c and escaping backslashes in script path" >&2 #flags to cmd.exe such as /c are interpreted by msys as looking like a unix path #review - for nextshellpath targets specified in the block for win32 - we don't expect unix paths (?) #what about other flags? - can we just double up all forward slashes? #maint: keep this munging in sync with the tcl block and perl block which must also do msys munging nextshellpath="${nextshellpath// \/[cC] / \/\/c }" # echo "new nextshellpath: ${nextshellpath}" #review - #don't double quote this script=${script//\\/\\\\} fi echo "calling ${nextshellpath} $script $@" #load into array cmd_array=($nextshellpath) cmd_array+=("$script") #add script, which may contain spaces as a single entry ? cmd_array+=( "$@" ) #add each element of args to array as a separate entry (equiv ? "${arr[@]}") # printf "%s\n" "${cmd_array[@]}" "${cmd_array[@]}" # this works to make nextshellpath run - but joins $@ members incorrectly #eval ${nextshellpath} "$script" "$@" else #e.g /usr/bin/env tclsh "$0" "$@" ${nextshellpath} "$script" "$@" fi exitcode=$? #echo "zsh/bash reporting exitcode: ${exitcode}" exit $exitcode #-- override exitcode example #exit 66 else #already in bash - don't launch another process or we would loop #echo "zsh/bash payload" : fi # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload #printf "start of bash or zsh code" # wdir="$(pwd)"; [ "$(pwd)" = "/" ] && wdir="" case "$0" in /*) scriptpath="${0}";; *) scriptpath="$wdir/${0#./}";; esac scriptdir="${scriptpath%/*}" scriptdir=$(realpath $scriptdir) scriptpath=$(realpath $scriptpath) basename=$(basename "$scriptpath") #e.g punk-runtime.bash scriptroot="${basename%.*}" #e.g "punk-runtime" #artifact server base url - overridable for mirrors/testing url_kitbase="${PUNKBIN_URL:-https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master}" #$OSTYPE varies in capitalization across for example zsh and bash #uname probably a more consistent bet arch=$(uname -m) #machine/architecture plat=$(uname -s) #platform/system #even though most of the platform prongs are very similar, #we keep the code separate so it can be tweaked easily for unexpected differences #each prong sets local_platform (the CANONICAL punkshell platform-DIR name - see #punk::platform / 'help platforms': cpu tokens normalized amd64->x86_64, #aarch64->arm64). Per-platform default fetch runtimes are NOT baked here - they #are the server's curated defaults.txt (a punkbin release decision). narch="$arch" case "$narch" in amd64) narch="x86_64";; aarch64|arm64) narch="arm64";; esac if [[ "$plat" = "Linux"* ]]; then if [[ "$arch" = "x86_64"* ]]; then local_platform="linux-x86_64" elif [[ "$narch" = "arm64" ]]; then #canonical arm64 (aarch64). punkbin's existing arm kit predates the #arm64 name and sits in linux-arm (the server's defaults.txt records #per-platform recommendations). local_platform="linux-arm64" elif [[ "$arch" = "arm"* ]]; then #32-bit arm local_platform="linux-arm" else local_platform="linux-$narch" fi os="linux" elif [[ "$plat" = "Darwin"* ]]; then os="macosx" #universal (multi-arch) binaries - the runtime tier keeps ONE macosx folder #(per-arch macosx-x86_64/macosx-arm64 names serve the lib tree tier) local_platform="macosx" elif [[ "$plat" = "FreeBSD"* ]]; then #canonical names (2026-07-22): freebsd-x86_64 (punkbin's actual folder; this #payload previously said freebsd-amd64) / freebsd-arm64 local_platform="freebsd-$narch" os="freebsd" elif [[ "$plat" == "DragonFly"* ]]; then local_platform="dragonflybsd-$narch" os="dragonflybsd" elif [[ "$plat" == "NetBSD"* ]]; then local_platform="netbsd-$narch" os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then local_platform="openbsd-$narch" os="openbsd" elif [[ "$plat" == "MINGW32"* ]]; then #32-bit msys shell - a genuine 32-bit machine reports i*86; a 32-bit shell #on a 64-bit OS is rare enough that the machine arch decides (REVIEW) os="win32" case "$arch" in i*86) local_platform="win32-ix86";; *) local_platform="win32-x86_64";; esac elif [[ "$plat" == "MINGW64"* ]]; then #REVIEW os="win32" local_platform="win32-x86_64" elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" case "$arch" in i*86) local_platform="win32-ix86";; *) local_platform="win32-x86_64";; esac elif [[ "$plat" == "MSYS_NT"* ]]; then echo MSYS os="win32" #use 'command -v' (shell builtin preferred over external which) interp=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` shellpath=`command -v $interp` shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname #"c:/windows/system32/" is quite likely in the path ahead of msys,git etc. #This breaks calls to various unix utils such as sed etc (wsl related?) export PATH="$shellfolder${PATH:+:${PATH}}" local_platform="win32-x86_64" else local_platform="other" os="other" fi #-- platform resolution (G-105 cross-build staging) --------------------------- #The prongs above detect the LOCAL platform. fetch/list/use accept #'-platform

' to aim at another punkbin platform-dir (cross-build staging / #provisioning); resolution: -platform arg > PUNK_RUNTIME_PLATFORM env > local. #Values are punkbin platform-dir names (e.g win32-x86_64, linux-x86_64, macosx) #- NOT zig triples (the buildsuite maps triples to platform dirs at build time). #Validation is shape-only: the server's sha1sums/404 is the truth for what #exists. 'run' is LOCAL ONLY (foreign binaries are not runnable here) - only a #LEADING -platform is rejected there; later args belong to the launched runtime. action="${1:-}" [[ $# -gt 0 ]] && shift platform_opt="" case "$action" in fetch|list|use|platforms) newargs=() while [[ $# -gt 0 ]]; do if [[ "$1" == "-platform" ]]; then shift if [[ $# -eq 0 ]]; then echo "option -platform requires a value (a punkbin platform-dir name, e.g linux-x86_64)" exit 1 fi platform_opt="$1" else newargs+=("$1") fi shift done set -- ${newargs[@]+"${newargs[@]}"} ;; run) if [[ "${1:-}" == "-platform" ]]; then echo "'run' launches the LOCAL platform's active runtime - it takes no -platform option" echo "(foreign-platform folders are cross-build staging; deploy them to their platform to run)" exit 1 fi ;; esac platform="${platform_opt:-${PUNK_RUNTIME_PLATFORM:-$local_platform}}" if ! [[ "$platform" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then echo "invalid platform name '$platform' (expected a punkbin platform-dir name such as win32-x86_64, linux-x86_64, macosx)" exit 1 fi is_local=0 [[ "$platform" == "$local_platform" ]] && is_local=1 archdir="${scriptdir}/runtime/${platform}" archtail="$platform" #server folder name e.g linux-x86_64 active_file="${archdir}/active.toml" #sha1 of a file - tool varies by platform (sha1sum: linux/coreutils, shasum: macosx, #sha1: BSDs, openssl: common fallback). Empty result means no tool available. sha1_of() { if command -v sha1sum >/dev/null 2>&1; then sha1sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 1 "$1" | awk '{print $1}' elif command -v sha1 >/dev/null 2>&1; then sha1 -q "$1" elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha1 -r "$1" | awk '{print $1}' else echo "" fi } lcase() { printf '%s' "$1" | tr 'A-F' 'a-f'; } #active runtime marker: constrained single-key toml, per platform folder #(local per-machine state - ignored by git and fossil) get_active() { if [[ -f "$active_file" ]]; then sed -n 's/^[[:space:]]*active[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$active_file" | head -n 1 fi } set_active() { printf 'active = "%s"\n' "$1" > "$active_file" echo "active runtime for $archtail set to: $1 (recorded in $active_file)" } #installed runtime candidates - exclude directories, build copies and #non-runtime files (ps1-payload parity: files only, same extension excludes). #LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order #matches the ps1 payload's ordinal sorting, so listings agree everywhere. list_candidates() { local f LC_ALL=C if [[ -d "$archdir" ]]; then for f in "$archdir"/*; do [[ -f "$f" ]] || continue f="${f##*/}" case "$f" in *_BUILDCOPY*|*.txt|*.toml|*.tm|*.tmp|*.log) continue;; esac printf '%s\n' "$f" done fi } #name minus a .exe suffix only - dotted tcl patchlevels (tclsh9.0.5-punk) make #generic last-dot extension stripping wrong for extensionless unix names rootname_of() { case "$1" in *.exe|*.EXE) printf '%s' "${1%.???}";; *) printf '%s' "$1";; esac } #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Prints a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. metadata_summary() { local exename="$1" tomlfile parts variant tclpatch revision piperepl artname target tomlfile="$archdir/$(rootname_of "$exename").toml" [[ -f "$tomlfile" ]] || return 0 variant=$(sed -n 's/^[[:space:]]*variant[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1) tclpatch=$(sed -n 's/^[[:space:]]*tcl_patchlevel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1) revision=$(sed -n 's/^[[:space:]]*revision[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$tomlfile" | head -n 1) piperepl=$(sed -n 's/^[[:space:]]*piperepl[[:space:]]*=[[:space:]]*\(true\|false\).*/\1/p' "$tomlfile" | head -n 1) artname=$(sed -n 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1) target=$(sed -n 's/^[[:space:]]*target[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1) parts="" [[ -n "$variant" ]] && parts="$parts variant=$variant" [[ -n "$tclpatch" ]] && parts="$parts tcl=$tclpatch" [[ -n "$revision" ]] && parts="$parts r$revision" if [[ "$piperepl" == "true" ]]; then parts="$parts piperepl=on" elif [[ "$piperepl" == "false" ]]; then parts="$parts piperepl=off"; fi #a materialized working copy records which immutable artifact it came from [[ -n "$artname" && "$artname" != "$exename" ]] && parts="$parts from=$artname" #integrity flag: a runtime filed under a platform folder its metadata says it #was not built for (cross-platform fetch/staging misfiling) [[ -n "$target" && "$target" != "$archtail" ]] && parts="$parts !TARGET-MISMATCH:$target" [[ -n "$parts" ]] && printf '[%s]' "${parts# }" } #operator help (the 'help' action; the no-args case shows show_usage only). #Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage. show_usage() { echo "Usage: $0 {fetch|list|use|run|platforms|help}" echo " fetch ?? ?-platform

? download+verify a runtime from punkbin" echo " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" echo " use ?-platform

? select active / materialize -r artifact" echo " run ?args...? launch the active local-platform runtime" echo " platforms ?-remote? local platform folders / platforms the server serves" echo " help full help (options, env vars, examples)" } show_help() { echo "punk-runtime - manage the plain Tcl runtimes under bin/runtime//" echo "" show_usage echo "" echo "Actions:" echo " fetch ?? ?-platform

?" echo " Download a runtime from the punkbin artifact server into" echo " bin/runtime/

/ with sha1 verification against the server's" echo " sha1sums.txt. -r-named family artifacts also fetch their" echo " .toml metadata record. Omitting fetches the" echo " platform's recommended default per the server's curated" echo " defaults.txt (a punkbin release decision; works with -platform" echo " too) - platforms without a recorded default need an explicit" echo " ." echo " list ?-remote? ?-platform

?" echo " List installed runtimes with artifact-metadata summaries" echo " (variant, tcl patchlevel, revision, piperepl policy, source" echo " artifact of a materialized copy; a !TARGET-MISMATCH flag marks" echo " a runtime filed under the wrong platform folder). -remote" echo " compares local runtimes against the server's sha1sums, marks" echo " the locally ACTIVE runtime (*) and annotates the platform's" echo " recommended runtime ((server default), per the server's" echo " defaults.txt when available) - so active-vs-default is" echo " visible at a glance." echo " use ?-platform

?" echo " Select the runtime that 'run' launches (records active.toml in" echo " the platform folder). An immutable -r ARTIFACT name is" echo " MATERIALIZED into its WORKING name (name minus -r - what" echo " mapvfs and projects reference), metadata toml copied alongside." echo " run ?args...?" echo " Launch the ACTIVE runtime of the LOCAL platform, passing args." echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" echo " installed candidate. Takes no -platform (foreign binaries are" echo " not runnable here)." echo " platforms ?-remote?" echo " List local platform folders under bin/runtime/. With -remote," echo " list the platforms the artifact server serves - read from the" echo " server's platforms.txt discovery manifest (part of the punkbin" echo " layout; third-party mirrors carry the same file), with local" echo " presence marked. Servers without the manifest get an actionable" echo " message (name platforms explicitly with -platform)." echo "" echo "Platforms: punkbin platform-dir names (e.g win32-x86_64, linux-x86_64," echo " macosx) - not zig triples. Default is the local platform; override" echo " order: -platform argument > PUNK_RUNTIME_PLATFORM env var." echo " Foreign-platform folders serve cross-build staging and provisioning:" echo " active.toml travels with the folder when it is deployed to its real" echo " platform (unix exec bits are restored at deploy time - e.g rsync/tar/" echo " chmod on the receiving side)." echo "" echo "Env: PUNKBIN_URL (artifact server base url), PUNK_RUNTIME_PLATFORM," echo " PUNK_ACTIVE_RUNTIME (run-time selection override)" echo "" echo "Examples:" echo " $0 platforms -remote" echo " $0 fetch tclsh9.0.5-punk-r1.exe" echo " $0 use tclsh9.0.5-punk-r1.exe (materializes tclsh9.0.5-punk.exe)" echo " $0 list -remote -platform linux-x86_64" echo " $0 fetch tclsh9.0.5-punk-r1 -platform linux-x86_64" echo " $0 use tclsh9.0.5-punk-r1 -platform linux-x86_64" echo " $0 run myscript.tcl arg1 arg2" } #G-103 artifact-tier names carry -r before the (optional) .exe suffix; prints #the WORKING name (artifact minus -r) when the name is artifact-tier, else "" working_name_of() { local w w=$(printf '%s' "$1" | sed -E 's/^(.*)-r[0-9]+(\.[Ee][Xx][Ee])?$/\1\2/') if [[ "$w" != "$1" ]]; then printf '%s' "$w"; fi } case "$action" in "fetch") if [[ "$archtail" == "other" ]]; then echo "Unrecognised local platform - name one explicitly with -platform" echo "(canonical names: 'help platforms' in the punk shell)" exit 1 fi runtime="${1:-}" if [[ -z "$runtime" ]]; then #no name given: consult the server's curated defaults.txt - the punkbin #release recommendation (updated there as part of each publication #change-set; see punkbin AGENTS.md). Per-platform server data, so it #works for any -platform, not just local. defaultsurl="${url_kitbase}/defaults.txt" defaultslocal="${scriptdir}/runtime/defaults.txt" mkdir -p "${scriptdir}/runtime" if ! curl -fsSL --output "$defaultslocal" "$defaultsurl"; then if [[ -f "$defaultslocal" ]]; then echo "WARNING: could not fetch $defaultsurl - using cached copy at $defaultslocal" else echo "Could not fetch $defaultsurl" echo "No server default available - name a runtime explicitly:" echo " $0 fetch ?-platform $archtail?" echo " (see available names: $0 list -remote -platform $archtail)" exit 1 fi fi runtime=$(awk -v p="$archtail" '$1==p {print $2; exit}' "$defaultslocal") if [[ -z "$runtime" ]]; then echo "No default recorded for platform '$archtail' in the server's defaults.txt - name a runtime explicitly:" echo " $0 fetch ?-platform $archtail?" echo " (see available names: $0 list -remote -platform $archtail)" exit 1 fi echo "using server default for $archtail: $runtime" fi if [[ -n "$runtime" ]]; then url="${url_kitbase}/${archtail}/${runtime}" output="${archdir}/${runtime}" sha1url="${url_kitbase}/${archtail}/sha1sums.txt" sha1local="${archdir}/sha1sums.txt" mkdir -p "$archdir" echo "Fetching $sha1url" if ! curl -fsSL --output "$sha1local" "$sha1url"; then echo "An error occurred while downloading $sha1url" if [[ -f "$output" ]]; then echo "A runtime is available at $output - but we failed to retrieve the list of sha1sums from the server" echo "Unable to check for updated version at this time." else echo "Please retry - or manually download a runtime from ${url_kitbase}/${archtail} and verify checksums" fi exit 1 fi #sha1sums.txt line format: * (binary indicator) #(dots in $runtime are regex-any here - acceptable for these filenames) stored_sha1=$(grep -E "^[0-9a-fA-F]{40} \*${runtime}\$" "$sha1local" | head -n 1 | cut -d' ' -f1) if [[ -z "$stored_sha1" ]]; then echo "Unable to locate hash for $runtime in $sha1local - Aborting" echo "Please download and verify manually (or check available names with: $0 list)" exit 1 fi probe=$(sha1_of "$sha1local") if [[ -z "$probe" ]]; then echo "No sha1 tool found (tried sha1sum, shasum, sha1, openssl) - refusing unverified download." echo "Please install one (e.g coreutils or openssl) and retry." exit 1 fi need_download=1 if [[ -f "$output" ]]; then echo "Runtime already found at $output" echo "Checking sha1 checksum of local file versus sha1 of server file" local_sha1=$(sha1_of "$output") if [[ "$(lcase "$local_sha1")" == "$(lcase "$stored_sha1")" ]]; then need_download=0 echo "Local copy of runtime at $output matches sha1 checksum of file on server." echo "No download required" else echo "$runtime on server has different sha1 hash - Download required" fi else echo "$runtime not found locally - Download required" fi if [[ "$need_download" -eq 1 ]]; then echo "Downloading from $url ..." if ! curl -fSL --output "${output}.tmp" "$url"; then echo "Error: Failed to download to ${output}.tmp" exit 1 fi echo "comparing sha1 checksum of downloaded file with data in sha1sums.txt" new_sha1=$(sha1_of "${output}.tmp") if [[ "$(lcase "$new_sha1")" == "$(lcase "$stored_sha1")" ]]; then echo "sha1 checksum ok" mv -f "${output}.tmp" "$output" chmod +x "$output" echo "Runtime is available at $output" if [[ "$plat" == "Linux"* ]]; then echo "Please ensure libxFt.so.2 is available" echo "e.g on Ubuntu: sudo apt-get install libxft2" fi else echo "WARNING! sha1 of downloaded file at ${output}.tmp does not match stored sha1 from sha1sums.txt" echo "The .tmp file has been left in place for inspection - it has NOT been installed." exit 1 fi fi #G-103: family artifacts (-r names) carry a metadata toml alongside #on the server - fetch it too; absence is fine (pre-family runtimes) if [[ -n "$(working_name_of "$runtime")" ]]; then tomlname="$(rootname_of "$runtime").toml" if curl -fsSL --output "${archdir}/${tomlname}" "${url_kitbase}/${archtail}/${tomlname}"; then echo "artifact metadata saved at ${archdir}/${tomlname}" else rm -f "${archdir}/${tomlname}" echo "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" fi fi #first fetch establishes the active runtime; later fetches never steal it if [[ -z "$(get_active)" ]]; then set_active "$runtime" fi fi ;; "list") if [[ "${1:-}" == "-remote" ]]; then #compare local runtimes with those listed on the artifact server #(functional parity with the powershell payload's 'list -remote') sha1url="${url_kitbase}/${archtail}/sha1sums.txt" sha1local="${archdir}/sha1sums.txt" mkdir -p "$archdir" if curl -fsSL --output "$sha1local" "$sha1url"; then echo "Fetched $sha1url" elif [[ -f "$sha1local" ]]; then echo "WARNING: could not fetch $sha1url - using cached copy at $sha1local" else echo "Unable to fetch $sha1url and no cached copy available" exit 1 fi probe=$(sha1_of "$sha1local") if [[ -z "$probe" ]]; then echo "No sha1 tool found (tried sha1sum, shasum, sha1, openssl) - cannot compare local runtimes." exit 1 fi #server-default + local-active surfacing (G-103): the server's curated #defaults.txt names the recommended runtime per platform - best-effort #fetch (cached/absent both fine for a listing), then mark the matching #row and the locally active one so "is my active the recommended #default?" is answerable at a glance. activename=$(get_active) platform_default="" defaultslocal="${scriptdir}/runtime/defaults.txt" curl -fsL --silent --output "$defaultslocal" "${url_kitbase}/defaults.txt" 2>/dev/null || : if [[ -f "$defaultslocal" ]]; then platform_default=$(awk -v p="$archtail" '$1==p {print $2; exit}' "$defaultslocal") fi echo "-----------------------------------------------------------------------" echo "Runtimes for $archtail" echo "Local $archdir" echo "Remote ${url_kitbase}/${archtail}" if [[ -n "$platform_default" ]]; then echo "server default for $archtail: $platform_default" fi if [[ -n "$activename" ]]; then #the default names an immutable -r ARTIFACT while the active is #typically its materialized WORKING name - the beside-toml's 'name' #field records which artifact the working copy came from, so match #on either identity active_artifact="" activetoml="$archdir/$(rootname_of "$activename").toml" if [[ -f "$activetoml" ]]; then active_artifact=$(sed -n 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$activetoml" | head -n 1) fi matchnote="" if [[ -n "$platform_default" ]] && [[ "$activename" == "$platform_default" || "$active_artifact" == "$platform_default" ]]; then matchnote=" (= server default)" fi echo "active (local): $activename$matchnote" fi echo "-----------------------------------------------------------------------" echo " Local Remote" echo "-----------------------------------------------------------------------" for f in $(list_candidates); do local_sha1=$(lcase "$(sha1_of "$archdir/$f")") stored_sha1=$(grep -E "^[0-9a-fA-F]{40} \*${f}\$" "$sha1local" | head -n 1 | cut -d' ' -f1) if [[ -z "$stored_sha1" ]]; then rhs="(not listed on server)" elif [[ "$local_sha1" == "$(lcase "$stored_sha1")" ]]; then rhs="Same version" else rhs="UPDATE AVAILABLE" fi mark=" " [[ "$f" == "$activename" ]] && mark="* " annot="" [[ -n "$platform_default" && "$f" == "$platform_default" ]] && annot=" (server default)" printf '%s%-35s %s%s\n' "$mark" "$f" "$rhs" "$annot" done #remote-only entries, sorted (ps1-payload parity - its dict enumeration #is explicitly sorted). tr strips CRLF \r: msys grep strips it in text #mode but bash read does not - without this a locally-present runtime #also shows as a remote-only row (\r-suffixed name fails the -f test) tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do #skip support files riding in the server's sha1sums beside the #runtimes (artifact metadata tomls etc) - same extension set the #local candidate filter excludes case "$rname" in *.txt|*.toml|*.tm|*.tmp|*.log) continue;; esac if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then annot="" [[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)" printf ' %-35s %s%s\n' "-" "$rname" "$annot" fi done echo "-----------------------------------------------------------------------" echo "* = active (local selection)" else if [[ -d "$archdir" ]]; then candidates=$(list_candidates) active=$(get_active) count=$(printf '%s\n' "$candidates" | grep -c . ) echo "$count runtime(s) in $archdir" for f in $candidates; do meta=$(metadata_summary "$f") if [[ "$f" == "$active" ]]; then printf '* %-35s (active) %s\n' "$f" "$meta" else printf ' %-35s %s\n' "$f" "$meta" fi done if [[ -n "$active" && ! -f "$archdir/$active" ]]; then echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use ' to reselect" fi echo "Use: '$0 list -remote' to compare local runtimes with those available on the artifact server" echo "Use: '$0 use ' to select the runtime that 'run' launches" echo "Use: '$0 use -name>' to materialize an immutable -r artifact into its working name and select it" else echo "No runtimes available in $archdir" echo " Use '$0 fetch' to install." fi fi ;; "use") #with -platform: manage THAT platform folder's selection/materialization #(cross-build staging - active.toml travels with the folder when deployed; #'run' on this machine only ever consults the local platform) if [[ -z "${1:-}" ]]; then echo "Usage: $0 use ?-platform

?" echo "Installed candidates ($platform):" for f in $(list_candidates); do echo " $f" done active=$(get_active) if [[ -n "$active" ]]; then echo "Currently active: $active" fi exit 1 fi case "$1" in *_BUILDCOPY|*.txt|*.toml|*.tm|*.tmp) echo "'$1' is not a selectable runtime" exit 1 ;; esac if [[ ! -f "$archdir/$1" ]]; then echo "No runtime named '$1' found in $archdir" echo "Installed candidates:" for f in $(list_candidates); do echo " $f" done exit 1 fi #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects #the working name. Republishing artifacts never churns consumers. #(chmod is a no-op when staging unix runtimes from windows filesystems - #exec bits are restored at deploy time, e.g rsync/tar/chmod on the guest.) working=$(working_name_of "$1") if [[ -n "$working" ]]; then cp -f "$archdir/$1" "$archdir/$working" chmod +x "$archdir/$working" srctoml="$archdir/$(rootname_of "$1").toml" desttoml="$archdir/$(rootname_of "$working").toml" if [[ -f "$srctoml" ]]; then cp -f "$srctoml" "$desttoml" echo "materialized $working from artifact $1 (metadata toml copied alongside)" else echo "materialized $working from artifact $1 (no metadata toml found beside the artifact)" fi set_active "$working" else set_active "$1" fi ;; "run") #resolution order: PUNK_ACTIVE_RUNTIME env override, active.toml, single #installed candidate - otherwise error with candidates (no last-in-list guessing). #LOCAL platform only: the -platform arg is rejected in the scan above, and a #PUNK_RUNTIME_PLATFORM env override is likewise ignored here (foreign #binaries are not runnable; the env override serves list/fetch/use). if [[ "$is_local" -ne 1 ]]; then archdir="${scriptdir}/runtime/${local_platform}" active_file="${archdir}/active.toml" fi activeruntime="" if [[ -n "$PUNK_ACTIVE_RUNTIME" ]]; then activeruntime="$PUNK_ACTIVE_RUNTIME" if [[ ! -f "$archdir/$activeruntime" ]]; then echo "PUNK_ACTIVE_RUNTIME '$activeruntime' not found in $archdir" exit 1 fi else activeruntime=$(get_active) if [[ -n "$activeruntime" && ! -f "$archdir/$activeruntime" ]]; then echo "active runtime '$activeruntime' (from $active_file) is not present in $archdir" echo "Reselect with: $0 use (or fetch it: $0 fetch $activeruntime)" exit 1 fi if [[ -z "$activeruntime" ]]; then candidates=$(list_candidates) count=$(printf '%s\n' "$candidates" | grep -c . ) if [[ "$count" -eq 1 ]]; then activeruntime="$candidates" elif [[ "$count" -eq 0 ]]; then echo "No runtimes seem to be installed in $archdir" echo "Please use '$0 fetch' to install." exit 1 else echo "Multiple runtimes installed and no active runtime selected." echo "Select one with: $0 use " echo "Installed candidates:" for f in $candidates; do echo " $f" done exit 1 fi fi fi activeruntime_fullpath="$archdir/$activeruntime" #echo "using $activeruntime_fullpath" #(the action was already shifted off during the option scan - "$@" is #exactly the runtime's argument list) $activeruntime_fullpath "$@" ;; "platforms") #enumerate platform folders: local (bin/runtime/*) and, with -remote, the #server's platforms.txt discovery manifest (raw-file servers have no #directory listing - the manifest is part of the punkbin layout contract; #third-party mirrors carry the same file). -platform is ignored here #(this action enumerates ALL platforms). rtroot="${scriptdir}/runtime" localdirs="" if [[ -d "$rtroot" ]]; then localdirs=$(LC_ALL=C ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) fi if [[ "${1:-}" == "-remote" ]]; then manifesturl="${url_kitbase}/platforms.txt" manifestlocal="${rtroot}/platforms.txt" mkdir -p "$rtroot" if curl -fsSL --output "$manifestlocal" "$manifesturl"; then echo "Fetched $manifesturl" elif [[ -f "$manifestlocal" ]]; then echo "WARNING: could not fetch $manifesturl - using cached copy at $manifestlocal" else echo "No platforms.txt available from $manifesturl" echo "(pre-convention punkbin or third-party mirror without the discovery manifest)" echo "Name platforms explicitly with -platform; canonical names: 'help platforms' in the punk shell" exit 1 fi echo "-----------------------------------------------------------------------" echo "Platforms served by ${url_kitbase}" echo "-----------------------------------------------------------------------" remoteplatforms="" while IFS= read -r line; do line="${line#"${line%%[![:space:]]*}"}" [[ -z "$line" || "${line:0:1}" == "#" ]] && continue remoteplatforms="$remoteplatforms $line" marks="" [[ "$line" == "$local_platform" ]] && marks="local platform" if printf '%s\n' $localdirs | grep -qx "$line"; then [[ -n "$marks" ]] && marks="$marks, " marks="${marks}local dir present" fi if [[ -n "$marks" ]]; then printf ' %-25s (%s)\n' "$line" "$marks" else printf ' %-25s\n' "$line" fi done < "$manifestlocal" for d in $localdirs; do if ! printf '%s\n' $remoteplatforms | grep -qx "$d"; then printf ' %-25s (local dir only - not served remotely)\n' "$d" fi done echo "-----------------------------------------------------------------------" echo "Use: '$0 list -remote -platform ' to see a platform's runtimes" else echo "-----------------------------------------------------------------------" echo "Local platform folders under $rtroot" echo "-----------------------------------------------------------------------" if [[ -z "$localdirs" ]]; then echo " (none - 'fetch' creates the local platform's folder)" fi for d in $localdirs; do if [[ "$d" == "$local_platform" ]]; then printf '* %-25s (local platform)\n' "$d" else printf ' %-25s\n' "$d" fi done echo "-----------------------------------------------------------------------" echo "Use: '$0 platforms -remote' to see platforms served by the artifact server" echo "Canonical platform names: 'help platforms' in the punk shell" fi ;; "help") show_help ;; "") #no action: short usage + pointer at the fuller help show_usage echo "" echo "'$0 help' gives options, env vars and examples." ;; *) echo "unknown action '$action'" show_usage exit 1 ;; esac #(512B spacer) byte-alignment padding so template labels beyond the payloads clear cmd's #512-byte label-scan boundaries - resize this comment if scriptwrap checkfile reports a #boundary-spanning label after a payload edit (see bin/AGENTS.md polyglot workflow) # #printf "zsh/bash done \n" # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh 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 my $exit_code = 0; use Cwd qw(abs_path); my $scriptname = abs_path($0); #print "perl $scriptname\n"; my $os = "$^O"; if ($os eq "MSWin32") { $os = "win32"; } elsif ($os eq "darwin") { $os = "macosx"; } print "os $os\n"; # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload #use ExtUtils::Installed; #my $installed = ExtUtils::Installed->new(); #my @modules = $installed->modules(); #print "Modules:\n"; #foreach my $m (@modules) { # print "$m\n"; #} # -- --- --- my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; } # print STDERR "No perl code for this script. Try another program such as tcl or bash"; # # -- --- --- --- --- --- --- --- #$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 # ## ### ### ### ### ### ### ### ### ### ### ### ### ### #$MyInvocation.ScriptName should probably be considered deprecated # https://stackoverflow.com/questions/78511229/how-can-i-choose-between-myinvocation-scriptname-and-myinvocation-pscommandpat $runningscriptname = $PSCommandPath if (-not $MyInvocation.PSCommandPath) { $callingscriptname = '' } else { $callingscriptname = $MyInvocation.PSCommandPath } #The problem with psmodulepath #https://github.com/PowerShell/PowerShell/issues/18108 # psmodulepath is shared by powershell and pwsh despite not all ps modules being compatible. # It is futzed with by powershell/pwsh based on detecting the child process type. # a psmodulepath that has been futzed with by pwsh will not work for a child powershell 5 process that isn't launched directly #This is inherently unfriendly to situations where an intervening process may be something else such as cmd.exe,tcl,perl etc # nevertheless, powershell/pwsh maintainers seem to have taken the MS-centric view of the world that such situations don't exist :/ # #symptoms of these shenannigans not working include things like Get-FileHash failing in powershell desktop # #We don't know if the original console was pwsh/powershell or cmd.exe, and we need to potentially divert to powershell 5 (desktop) #via tcl or perl etc - or cmd.exe if ($PSVersionTable.PSVersion.Major -le 5) { # For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath #snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e #Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder?? $env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';' } 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 } } # Example usage: # 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, # [Parameter(ValueFromRemainingArguments)] $opts # ) #} #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) { # 'myargument' { # Set-Variable -Name $_ -Value $PSBoundParameters."$_" # } # 'opts' { # write-warning "Unused parameters: $($PSBoundParameters.$_)" # } # Default { # write-warning "Unhandled parameter -> [$($_)]" # } # } # foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { # #... # } # } # end { # #Main function logic # Write-Host "myargument value is: $myargument" # #myotherfunction @PSBoundParameters # } #} #psmain @args #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Running Script Name : {0}" -f $runningscriptname | write-host "Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- $thisfileContent = Get-Content $runningscriptname -Raw $startTag = ": <>" $endTag = ": <>" $pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" $match = [regex]::Match($thisfileContent,$pattern) $asadmin = 0 if ($match.Success) { $admininfo = $match.Groups[1].Value $asadmin = $admininfo.Contains("asadmin=1") if ($asadmin) { if ($args[0] -eq "PUNK-ELEVATED") { # May be present if launch and elevation was done via cmd.exe script # shift away first arg $newargs = $args | Select-Object -Skip 1 } else { $newargs = $args } # -Wait e.g for starting a service or other operations which remainder of script may depend on $arguments = @("-NoProfile","-NoLogo", "-NoExit", "-ExecutionPolicy", "Bypass") $arguments += @("-File", $($MyInvocation.MyCommand.Path)) foreach ($a in $newargs) { if ($a -match '\s') { $arguments += "`"$a`"" } else { $arguments += $a } } if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { # If not elevated, relaunch with elevated privileges Write-Host "Powershell elevating using start-process with -Verb RunAs" if ($PSVersionTable.PSEdition -eq 'Core') { Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -Wait -Verb RunAs } else { Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -Wait -Verb RunAs } Exit # Exit the current non-elevated process } else { if ($args[0] -eq "PUNK-ELEVATED") { #Already elevated (by cmd.exe) #.. but it is impossible to modify or reassign the automatic $args variable # so let's start yet another whole new process just to remove one leading argument so the custom script can operate on parameters cleanly - thanks powershell :/ if ($PSVersionTable.PSEdition -eq 'Core') { Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -NoNewWindow -Wait } else { Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -NoNewWindow -Wait } Exit } } } } # $startTag = ": <>" $endTag = ": <>" $pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" $match = [regex]::Match($thisfileContent,$pattern) if ($match.Success) { $plat = [System.Environment]::OSVersion.Platform if ($plat -eq "Unix") { $runtime_ident = [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier switch ($runtime_ident.split("-")[0]) { "freebsd" { # untested $os = "freebsd" } "linux" { $os = "linux" } "osx" { # osx-x64 or osx-arm64 ? $os = "macosx" } default { #openbsd, netbsd ? $os = "other" } } } else { $os = "win32" } $matchedlines = $match.Groups[1].Value $nextshell_type = "" $nextshell_path = "" ForEach ($line in $($matchedlines -split "\r?\n")) { $m = [regex]::Match($line,".*nextshelltype\[${os}[_]+\]=([^_]*)[_]*") if ($m.Success) { $nextshell_type = $m.Groups[1].Value } $m = [regex]::Match($line,".*nextshellpath\[${os}[_]+\]=([^_]*)[_]*") if ($m.Success) { $nextshell_path = $m.Groups[1].Value } if ($nextshell_type -ne "" -and $nextshell_path -ne "") { break } } if (-not (("pwsh", "powershell", "") -contains $nextshell_type)) { #nextshell diversion exists for this platform write-host "os: $os pwsh/powershell launching subshell of type: $nextshell_type shellpath: $nextshell_path on script $runningscriptname" # $arguments = @($($MyInvocation.MyCommand.Path)) # $arguments += $args # NOTE - this gives incorrect argument quoting e.g wrong number of arguments received by launched process for arguments: a "b c" # $process = (Start-Process -FilePath $nextshell_path -ArgumentList $arguments -NoNewWindow -Wait) # Exit $process.ExitCode & $nextshell_path $runningscriptname $args exit $LASTEXITCODE } } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload # 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-Object -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 } } function ParameterDefinitions { param( [Parameter(ValueFromRemainingArguments=$true,Position = 1)][string[]] $opts ) } #active runtime marker: constrained single-key toml (active = "name") per platform #folder - local per-machine state, ignored by git and fossil function Get-PunkActiveRuntime { param([string] $archfolder) $activefile = Join-Path -Path $archfolder -ChildPath "active.toml" if (Test-Path -Path $activefile -PathType Leaf) { foreach ($line in (Get-Content -Path $activefile)) { $match = [regex]::Match($line, '^\s*active\s*=\s*"(.*)"\s*$') if ($match.Success) { return $match.Groups[1].Value } } } return "" } function Set-PunkActiveRuntime { param([string] $archfolder, [string] $name) $activefile = Join-Path -Path $archfolder -ChildPath "active.toml" #LF-terminated, no BOM - the bash payload reads the same file [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) Write-Host "active runtime set to: $name (recorded in $activefile)" } #installed runtime candidates - exclude build copies and non-runtime files. #ORDINAL name order: culture-sensitive Sort-Object collates differently between #windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 #twin runs under the invoking shell) - and ordinal matches the bash payload's #LC_ALL=C ordering, so listings agree byte-for-byte everywhere. function Get-PunkRuntimeCandidates { param([string] $archfolder) if (-not (Test-Path -Path $archfolder -PathType Container)) { return @() } $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) if ($files.Count -le 1) { return $files } $byname = @{} foreach ($f in $files) { $byname[$f.Name] = $f } $names = @($byname.Keys) [array]::Sort($names, [System.StringComparer]::Ordinal) $out = @() foreach ($n in $names) { $out += $byname[$n] } return $out } #operator help (the 'help' action; the no-args case shows the Usage block only). #Keep in sync with the bash payload's show_help/show_usage. function Show-PunkRuntimeUsage { write-host "Usage: punk-runtime.cmd {fetch|list|use|run|platforms|help}" write-host " fetch ?? ?-platform

? download+verify a runtime from punkbin" write-host " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" write-host " use ?-platform

? select active / materialize -r artifact" write-host " run ?args...? launch the active local-platform runtime" write-host " platforms ?-remote? local platform folders / platforms the server serves" write-host " help full help (options, env vars, examples)" } function Show-PunkRuntimeHelp { write-host "punk-runtime - manage the plain Tcl runtimes under bin/runtime//" write-host "" Show-PunkRuntimeUsage write-host "" write-host "Actions:" write-host " fetch ?? ?-platform

?" write-host " Download a runtime from the punkbin artifact server into" write-host " bin/runtime/

/ with sha1 verification against the server's" write-host " sha1sums.txt. -r-named family artifacts also fetch their" write-host " .toml metadata record. Omitting fetches the" write-host " platform's recommended default per the server's curated" write-host " defaults.txt (a punkbin release decision; works with -platform" write-host " too) - platforms without a recorded default need an explicit" write-host " ." write-host " list ?-remote? ?-platform

?" write-host " List installed runtimes with artifact-metadata summaries" write-host " (variant, tcl patchlevel, revision, piperepl policy, source" write-host " artifact of a materialized copy; a !TARGET-MISMATCH flag marks" write-host " a runtime filed under the wrong platform folder). -remote" write-host " compares local runtimes against the server's sha1sums, marks" write-host " the locally ACTIVE runtime (*) and annotates the platform's" write-host " recommended runtime ((server default), per the server's" write-host " defaults.txt when available) - so active-vs-default is" write-host " visible at a glance." write-host " use ?-platform

?" write-host " Select the runtime that 'run' launches (records active.toml in" write-host " the platform folder). An immutable -r ARTIFACT name is" write-host " MATERIALIZED into its WORKING name (name minus -r - what" write-host " mapvfs and projects reference), metadata toml copied alongside." write-host " run ?args...?" write-host " Launch the ACTIVE runtime of the LOCAL platform, passing args." write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" write-host " installed candidate. Takes no -platform (foreign binaries are" write-host " not runnable here)." write-host " platforms ?-remote?" write-host " List local platform folders under bin/runtime/. With -remote," write-host " list the platforms the artifact server serves - read from the" write-host " server's platforms.txt discovery manifest (part of the punkbin" write-host " layout; third-party mirrors carry the same file), with local" write-host " presence marked. Servers without the manifest get an actionable" write-host " message (name platforms explicitly with -platform)." write-host "" write-host "Platforms: punkbin platform-dir names (e.g win32-x86_64, linux-x86_64," write-host " macosx) - not zig triples. Default is the local platform; override" write-host " order: -platform argument > PUNK_RUNTIME_PLATFORM env var." write-host " Foreign-platform folders serve cross-build staging and provisioning:" write-host " active.toml travels with the folder when it is deployed to its real" write-host " platform (unix exec bits are restored at deploy time - e.g rsync/tar/" write-host " chmod on the receiving side)." write-host "" write-host "Env: PUNKBIN_URL (artifact server base url), PUNK_RUNTIME_PLATFORM," write-host " PUNK_ACTIVE_RUNTIME (run-time selection override)" write-host "" write-host "Examples:" write-host " punk-runtime.cmd platforms -remote" write-host " punk-runtime.cmd fetch tclsh9.0.5-punk-r1.exe" write-host " punk-runtime.cmd use tclsh9.0.5-punk-r1.exe (materializes tclsh9.0.5-punk.exe)" write-host " punk-runtime.cmd list -remote -platform linux-x86_64" write-host " punk-runtime.cmd fetch tclsh9.0.5-punk-r1 -platform linux-x86_64" write-host " punk-runtime.cmd use tclsh9.0.5-punk-r1 -platform linux-x86_64" write-host " punk-runtime.cmd run myscript.tcl arg1 arg2" } #platform (target) resolution for fetch/list/use: -platform arg > PUNK_RUNTIME_PLATFORM #env > the local default. Values are punkbin/bin-runtime platform-DIR names (e.g #win32-x86_64, linux-x86_64, macosx) - NOT zig triples (the buildsuite maps triples to #platform dirs at build time; punk-runtime speaks only the punkbin tier). Validation is #deliberately shape-only: the server's sha1sums/404 is the truth for what exists. #This payload runs on windows - the local default is win32-x86_64, or win32-ix86 on #a GENUINE 32-bit windows host (PROCESSOR_ARCHITECTURE x86 with no #PROCESSOR_ARCHITEW6432 - a 32-bit shell on a 64-bit OS keeps the x86_64 default: #the runtime store serves what the OS can run). A windows-arm default becomes a #question when punkbin carries such a folder. $script:PunkLocalPlatform = "win32-x86_64" if ($env:PROCESSOR_ARCHITECTURE -eq 'x86' -and -not $env:PROCESSOR_ARCHITEW6432) { $script:PunkLocalPlatform = "win32-ix86" } function Resolve-PunkRuntimePlatform { param([string] $requested) $plat = $script:PunkLocalPlatform if ($requested) { $plat = $requested } elseif ($env:PUNK_RUNTIME_PLATFORM) { $plat = $env:PUNK_RUNTIME_PLATFORM } #-cnotmatch: platform-dir names are lowercase and the artifact-server URL path #is case-sensitive (default -notmatch is case-insensitive and would pass typos #like Win32-X86_64 through to a server 404); bash payload parity (=~ is #case-sensitive there) if ($plat -cnotmatch '^[a-z0-9][a-z0-9_-]*$') { write-host "invalid platform name '$plat' (expected a punkbin platform-dir name such as win32-x86_64, linux-x86_64, macosx)" exit 1 } return $plat } #name minus a .exe suffix only - dotted tcl patchlevels (tclsh9.0.5-punk) make #generic last-dot extension stripping wrong for extensionless unix names function Get-PunkRuntimeRootName { param([string] $name) if ($name -match '(?i)\.exe$') { return $name.Substring(0, $name.Length - 4) } return $name } #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. function Get-PunkRuntimeMetadataSummary { param([string] $archfolder, [string] $exename, [string] $expectedplatform = "") $tomlfile = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $exename) + ".toml") if (-not (Test-Path -Path $tomlfile -PathType Leaf)) { return "" } $fields = @{} foreach ($line in (Get-Content -Path $tomlfile)) { $m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"(.*)"\s*$') if ($m.Success) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value continue } $m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([0-9]+|true|false)\s*$') if ($m.Success) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value } } $parts = @() if ($fields.ContainsKey('variant')) { $parts += "variant=$($fields['variant'])" } if ($fields.ContainsKey('tcl_patchlevel')) { $parts += "tcl=$($fields['tcl_patchlevel'])" } if ($fields.ContainsKey('revision')) { $parts += "r$($fields['revision'])" } if ($fields.ContainsKey('piperepl')) { if ($fields['piperepl'] -eq 'true') { $parts += "piperepl=on" } else { $parts += "piperepl=off" } } #a materialized working copy records which immutable artifact it came from if ($fields.ContainsKey('name') -and $fields['name'] -ne $exename) { $parts += "from=$($fields['name'])" } #integrity flag: a runtime filed under a platform folder its metadata says it #was not built for (cross-platform fetch/staging misfiling) if ($expectedplatform -ne "" -and $fields.ContainsKey('target') -and $fields['target'] -ne $expectedplatform) { $parts += "!TARGET-MISMATCH:$($fields['target'])" } if ($parts.Count -eq 0) { return "" } return "[" + ($parts -join " ") + "]" } function psmain { [CmdletBinding()] #Empty param block (extra params can be added) param( [Parameter(Mandatory=$false, Position = 0)][string] $action = "" ) dynamicparam { if ($action -eq 'list' -or $action -eq 'platforms') { #shared dynamic params: -remote for both; -platform is meaningful to #'list' and ignored by 'platforms' (which enumerates ALL platforms) $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "listruntime" Mandatory = $false } $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributeCollection.Add($parameterAttribute) $dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'remote', [switch], $attributeCollection ) #-platform (G-105 cross-build staging): punkbin platform-dir name to list for $platformAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "listruntime" Mandatory = $false } $platformAttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $platformAttributeCollection.Add($platformAttribute) $dynParam2 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'platform', [string], $platformAttributeCollection ) $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $paramDictionary.Add('remote', $dynParam1) $paramDictionary.Add('platform', $dynParam2) return $paramDictionary } elseif ($action -eq 'fetch' -or $action -eq 'use') { #GetDynamicParamDictionary ParameterDefinitions $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "fetchruntime" Mandatory = $false Position = 1 } $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributeCollection.Add($parameterAttribute) $dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'runtime', [string], $attributeCollection ) #-platform (G-105 cross-build staging): punkbin platform-dir name to fetch #into / select within (default: the local platform) $platformAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "fetchruntime" Mandatory = $false } $platformAttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $platformAttributeCollection.Add($platformAttribute) $dynParam2 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'platform', [string], $platformAttributeCollection ) $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $paramDictionary.Add('runtime', $dynParam1) $paramDictionary.Add('platform', $dynParam2) return $paramDictionary } elseif ($action -eq 'run') { #GetDynamicParamDictionary ParameterDefinitions $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "runargs" Mandatory = $false ValueFromRemainingArguments = $true } $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributeCollection.Add($parameterAttribute) $dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'opts', [string[]], $attributeCollection ) $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $paramDictionary.Add('opts', $dynParam1) return $paramDictionary } else { #accept all args when action is unrecognised - so we can go to help anyway $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "invalidaction" Mandatory = $false ValueFromRemainingArguments = $true } $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributeCollection.Add($parameterAttribute) $dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new( 'opts', [string[]], $attributeCollection ) $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $paramDictionary.Add('opts', $dynParam1) return $paramDictionary } } process { #Called once - we get a single item being our PSBoundParameters dictionary #write-host "Bound Parameters:$($PSBoundParameters.Keys)" switch ($PSBoundParameters.keys) { 'action' { write-host "got action " $PSBoundParameters.action Set-Variable -Name $_ -Value $PSBoundParameters."$_" $known_actions = @("fetch", "list", "use", "run", "platforms", "help") if (-not($known_actions -contains $action)) { write-host "action '$action' not understood. Known_actions: $known_actions" exit 1 } } 'opts' { # write-warning "Unused parameters: $($PSBoundParameters.$_)" } Default { # write-warning "Unhandled parameter -> [$($_)]" } } #foreach ($boundparam in $PSBoundParameters.Keys) { # write-host "k: $boundparam" #} } end { # PSBoundParameters #write-host "action:'$action'" $outbase = $PSScriptRoot $outbase = Resolve-Path -Path $outbase #expected script location is the bin folder of a punk project $rtfolder = Join-Path -Path $outbase -ChildPath "runtime" #Binary artifact server url. (git is not ideal for this - but will do for now - todo - use artifact system within gitea?) #overridable for mirrors/testing - keep in sync with the bash payload $artifacturl = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master" if ($env:PUNKBIN_URL) { $artifacturl = $env:PUNKBIN_URL } switch ($action) { 'fetch' { $arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"] $archfolder = Join-Path -Path $rtfolder -ChildPath "$arch" $archurl = "$artifacturl/$arch" $sha1url = "$archurl/sha1sums.txt" foreach ($boundparam in $PSBoundParameters.Keys) { write-host "fetchopt: $boundparam $($PSBoundParameters[$boundparam])" } $runtime = "" if ( $PSBoundParameters["runtime"].Length ) { $runtime = $PSBoundParameters["runtime"] } else { #no name given: consult the server's curated defaults.txt - the #punkbin release recommendation (updated there as part of each #publication change-set; see punkbin AGENTS.md). Per-platform #server data, so it works for any -platform, not just local. $defaultsurl = "$artifacturl/defaults.txt" $defaultslocal = Join-Path -Path $rtfolder -ChildPath "defaults.txt" if (-not (Test-Path -Path $rtfolder -PathType Container)) { new-item -Path $rtfolder -ItemType Directory -force | out-null } try { Invoke-WebRequest -Uri $defaultsurl -OutFile $defaultslocal -ErrorAction Stop } catch { if (Test-Path -Path $defaultslocal -PathType Leaf) { Write-Host "WARNING: could not fetch ${defaultsurl}: $($_.Exception.Message)" Write-Host "WARNING: using cached copy at $defaultslocal" } else { Write-Host "Could not fetch ${defaultsurl}: $($_.Exception.Message)" Write-Host "No server default available - name a runtime explicitly:" Write-Host " punk-runtime.cmd fetch ?-platform ${arch}?" Write-Host " (see available names: punk-runtime.cmd list -remote -platform $arch)" exit 1 } } foreach ($line in (Get-Content -Path $defaultslocal)) { $line = $line.Trim() if ($line -eq "" -or $line.StartsWith("#")) { continue } $parts = -split $line if ($parts.Count -ge 2 -and $parts[0] -eq $arch) { $runtime = $parts[1] break } } if ($runtime -eq "") { Write-Host "No default recorded for platform '$arch' in the server's defaults.txt - name a runtime explicitly:" Write-Host " punk-runtime.cmd fetch ?-platform ${arch}?" Write-Host " (see available names: punk-runtime.cmd list -remote -platform $arch)" exit 1 } Write-Host "using server default for ${arch}: $runtime" } $fileurl = "$archurl/$runtime" $output = join-path -Path $archfolder -ChildPath $runtime $sha1local = join-path -Path $archfolder -ChildPath "sha1sums.txt" $container = split-path -Path $output -Parent new-item -Path $container -ItemType Directory -force #create with intermediary folders if not already present try { Write-Host "Fetching $sha1url" Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop Write-Host "sha1 saved at $sha1local" } catch { Write-Host "An error occurred while downloading ${sha1url}: $($_.Exception.Message)" if ($_.Exception.Response) { Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)" } if (Test-Path -Path $sha1local -PathType Leaf) { #this path always fell through to the cached copy - now say so Write-Host "WARNING: proceeding with cached copy at $sha1local" } } if (Test-Path -Path $sha1local -PathType Leaf) { $sha1Content = Get-Content -Path $sha1local $stored_sha1 = "" foreach ($line in $sha1Content) { #all sha1sums have * (binary indicator) - review $match = [regex]::Match($line,"(.*) [*]${runtime}$") if ($match.Success) { $stored_sha1 = $match.Groups[1].Value Write-host "stored hash from sha1sums.txt: $stored_sha1" break } } if ($stored_sha1 -eq "") { Write-Host "Unable to locate hash for $runtime in $sha1local - Aborting" Write-Host "Please download and verify manually" return } $need_download = $false if (Test-Path -Path $output -PathType Leaf) { Write-Host "Runtime already found at $output" Write-Host "Checking sha1 checksum of local file versus sha1 of server file" $file_sha1 = Get-FileHash -Path "$output" -Algorithm SHA1 if (${file_sha1}.Hash -ne $stored_sha1) { Write-Host "$runtime on server has different sha1 hash - Download required" $need_download = $true } } else { Write-Host "$runtime not found locally - Download required" $need_download = $true } if ($need_download) { Write-Host "Downloading from $fileurl ..." try { Invoke-WebRequest -Uri $fileurl -OutFile "${output}.tmp" -ErrorAction Stop Write-Host "Runtime saved at $output.tmp" } catch { Write-Host "An error occurred while downloading $fileurl $($_.Exception.Message)" if ($_.Exception.Response) { Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)" } return } Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt" Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?) $newfile_sha1 = Get-FileHash -Path "${output}.tmp" -Algorithm SHA1 if (${newfile_sha1}.Hash -eq $stored_sha1) { Write-Host "sha1 checksum ok" Move-Item -Path "${output}.tmp" -Destination "${output}" -Force Write-Host "Runtime is available at ${output}" } else { Write-Host "WARNING! sha1 of downloaded file at $output.tmp does not match stored sha1 from sha1sums.txt" return } } else { Write-Host "Local copy of runtime at $output seems to match sha1 checksum of file on server." Write-Host "No download required" } #G-103: family artifacts (-r names) carry a metadata toml alongside #on the server - fetch it too; absence is fine (pre-family runtimes) if ($runtime -match '^(.*)-r([0-9]+)(\.[Ee][Xx][Ee])?$') { $tomlname = (Get-PunkRuntimeRootName $runtime) + ".toml" $tomlurl = "$archurl/$tomlname" $tomllocal = Join-Path -Path $archfolder -ChildPath $tomlname try { Invoke-WebRequest -Uri $tomlurl -OutFile $tomllocal -ErrorAction Stop Write-Host "artifact metadata saved at $tomllocal" } catch { Write-Host "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)" } } #first fetch establishes the active runtime; later fetches never steal it if ((Get-PunkActiveRuntime $archfolder) -eq "") { Set-PunkActiveRuntime $archfolder $runtime } } else { Write-Host "Unable to consult local copy of sha1sums.txt at $sha1local" if (Test-Path -Path $output -PathType Leaf) { Write-Host "A runtime is available at $output - but we failed to retrieve the list of sha1sums from the server" Write-Host "Unable to check for updated version at this time." } else { Write-Host "Please retry - or manually download a runtime from $archurl and verify checksums" } } } 'use' { #select the active runtime for subsequent 'run' calls. With -platform: #manage THAT platform folder's selection/materialization (cross-build #staging - the active.toml travels with the folder when deployed; 'run' #on this machine only ever consults the local platform). $arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"] $archfolder = Join-Path -Path $rtfolder -ChildPath "$arch" $candidates = Get-PunkRuntimeCandidates $archfolder $rtname = "" if ( $PSBoundParameters["runtime"].Length ) { $rtname = $PSBoundParameters["runtime"] } if ($rtname -eq "") { write-host "Usage: punk-runtime.cmd use ?-platform

?" write-host "Installed candidates ($arch):" foreach ($f in $candidates) { write-host " $($f.Name)" } $current = Get-PunkActiveRuntime $archfolder if ($current -ne "") { write-host "Currently active: $current" } exit 1 } if (-not ($candidates | Where-Object Name -eq $rtname)) { write-host "No runtime named '$rtname' found in $archfolder" write-host "Installed candidates:" foreach ($f in $candidates) { write-host " $($f.Name)" } exit 1 } #G-103 artifact-tier names (-r, immutable): 'use' MATERIALIZES the #artifact into its WORKING name (name minus -r - what mapvfs and #projects reference), copies its metadata toml alongside, and selects #the working name. Republishing artifacts never churns consumers. $am = [regex]::Match($rtname, '^(.*)-r([0-9]+)(\.[Ee][Xx][Ee])?$') if ($am.Success) { $working = $am.Groups[1].Value + $am.Groups[3].Value $srcexe = Join-Path -Path $archfolder -ChildPath $rtname $destexe = Join-Path -Path $archfolder -ChildPath $working Copy-Item -Path $srcexe -Destination $destexe -Force $srctoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $rtname) + ".toml") $desttoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $working) + ".toml") if (Test-Path -Path $srctoml -PathType Leaf) { Copy-Item -Path $srctoml -Destination $desttoml -Force write-host "materialized $working from artifact $rtname (metadata toml copied alongside)" } else { write-host "materialized $working from artifact $rtname (no metadata toml found beside the artifact)" } Set-PunkActiveRuntime $archfolder $working } else { Set-PunkActiveRuntime $archfolder $rtname } } 'run' { #launch the active runtime, passing arguments. #resolution order: PUNK_ACTIVE_RUNTIME env override, active.toml, single #installed candidate - otherwise error with candidates (no last-in-list guessing). #LOCAL PLATFORM ONLY - a foreign platform's binaries are not runnable here. #Only a LEADING -platform is rejected: everything after 'run' belongs to #the runtime, so a later arg spelled -platform must pass through untouched. if ($PSBoundParameters.opts.Length -gt 0 -and $PSBoundParameters.opts[0] -eq '-platform') { write-host "'run' launches the LOCAL platform's active runtime - it takes no -platform option" write-host "(foreign-platform folders are cross-build staging; deploy them to their platform to run)" exit 1 } $arch = $script:PunkLocalPlatform $archfolder = Join-Path -Path $rtfolder -ChildPath "$arch" if (-not(Test-Path -Path $archfolder -PathType Container)) { write-host "No runtimes seem to be installed for $arch`nPlease use 'punk-runtime.cmd fetch' to install" } else { $dircontents = Get-PunkRuntimeCandidates $archfolder if ($dircontents.Count -gt 0) { #write-host "run.." write-host "num params: $($PSBoundParameters.opts.count)" $activename = "" if ($env:PUNK_ACTIVE_RUNTIME) { $activename = $env:PUNK_ACTIVE_RUNTIME if (-not (Test-Path -Path (Join-Path -Path $archfolder -ChildPath $activename) -PathType Leaf)) { write-host "PUNK_ACTIVE_RUNTIME '$activename' not found in $archfolder" exit 1 } } else { $activename = Get-PunkActiveRuntime $archfolder if ($activename -ne "" -and -not (Test-Path -Path (Join-Path -Path $archfolder -ChildPath $activename) -PathType Leaf)) { write-host "active runtime '$activename' (from active.toml) is not present in $archfolder" write-host "Reselect with: punk-runtime.cmd use (or fetch it: punk-runtime.cmd fetch $activename)" exit 1 } if ($activename -eq "") { if ($dircontents.Count -eq 1) { $activename = $dircontents[0].Name } else { write-host "Multiple runtimes installed and no active runtime selected." write-host "Select one with: punk-runtime.cmd use " write-host "Installed candidates:" foreach ($f in $dircontents) { write-host " $($f.Name)" } exit 1 } } } $active = Join-Path -Path $archfolder -ChildPath $activename write-host "using: $active" if ($PSBoundParameters.opts.Length -gt 0) { $optsType = $PSBoundParameters.opts.GetType() #method can only be called if .opts is not null write-host "type of opts: $($optsType.FullName)" foreach ($boundparam in $PSBoundParameters.opts) { write-host $boundparam } Write-Host "opts: $($PSBoundParameters.opts)" Write-Host "args: $args" Write-HOst "argscount: $($args.Count)" $arglist = @() foreach ($o in $PSBoundParameters.opts) { $oquoted = $o -replace '"', "`\`"" #$oquoted = $oquoted -replace "'", "`'" if ($oquoted -match "\s") { $oquoted = "`"$oquoted`"" } $arglist += @($oquoted) } $arglist = $arglist.TrimEnd(' ') write-host "arglist: $arglist" #$arglist = $PSBoundParameters.opts Start-Process -FilePath $active -ArgumentList $arglist -NoNewWindow -Wait } else { #powershell 5.1 and earlier can't accept an empty -ArgumentList value :/ !! #$arglist = @() #Start-Process -FilePath $active -ArgumentList $arglist -NoNewWindow -Wait #Start-Process -FilePath $active -ArgumentList "" -NoNewWindow -Wait Start-Process -FilePath $active -NoNewWindow -Wait } } else { write-host "No files found in $archfolder" write-host "No runtimes seem to be installed for $arch`nPlease use 'punk-runtime.cmd fetch' to install." } } } 'list' { #-platform lists another platform's folder/server dir (cross-build staging) $arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"] $archfolder = Join-Path -Path $rtfolder -ChildPath "$arch" $sha1local = join-path -Path $archfolder -ChildPath "sha1sums.txt" $archurl = "$artifacturl/$arch" $sha1url = "$archurl/sha1sums.txt" if ( $PSBoundParameters.ContainsKey('remote') ) { if (-not (test-path -Path $archfolder -Type Container)) { new-item -Path $archfolder -ItemType Directory -force #create with intermediary folders if not already present } write-host "Checking for available remote runtimes for $arch" Write-Host "Fetching $sha1url" #cached fallback (parity with the bash payload): an unreachable server #falls back to a previously fetched sha1sums.txt with a warning try { Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop Write-Host "sha1 saved at $sha1local" } catch { if (Test-Path -Path $sha1local -PathType Leaf) { Write-Host "WARNING: could not fetch ${sha1url}: $($_.Exception.Message)" Write-Host "WARNING: using cached copy at $sha1local" } else { Write-Host "Unable to fetch $sha1url and no cached copy available: $($_.Exception.Message)" if ($_.Exception.Response) { Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)" } return } } $sha1Content = Get-Content -Path $sha1local $remotedict = @{} foreach ($line in $sha1Content) { #all sha1sums have * (binary indicator) - review $match = [regex]::Match($line,"(.*) [*](.*)$") if ($match.Success) { $server_sha1 = $match.Groups[1].Value $server_rt = $match.Groups[2].Value $remotedict[$server_rt] = $server_sha1 } } $localdict = @{} if (test-path -Path $archfolder -Type Container) { #shared candidate filter (bash payload parity - its -remote loop #already uses list_candidates): excludes directories, build #copies and .txt/.toml/.tm/.tmp/.log support files, so #active.toml / metadata tomls / stray logs never show as #local runtimes in the comparison $dircontents = Get-PunkRuntimeCandidates $archfolder foreach ($f in $dircontents) { $local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1 $localdict[$f.Name] = ${local_sha1}.Hash } } #server-default + local-active surfacing (G-103): the server's #curated defaults.txt names the recommended runtime per platform - #best-effort fetch (cached/absent both fine for a listing), then #mark the matching row and the locally active one so "is my #active the recommended default?" is answerable at a glance. $activename = Get-PunkActiveRuntime $archfolder $platform_default = "" $defaultslocal = Join-Path -Path $rtfolder -ChildPath "defaults.txt" try { Invoke-WebRequest -Uri "$artifacturl/defaults.txt" -OutFile $defaultslocal -ErrorAction Stop } catch { } if (Test-Path -Path $defaultslocal -PathType Leaf) { foreach ($line in (Get-Content -Path $defaultslocal)) { $line = $line.Trim() if ($line -eq "" -or $line.StartsWith("#")) { continue } $parts = -split $line if ($parts.Count -ge 2 -and $parts[0] -eq $arch) { $platform_default = $parts[1] break } } } Write-host "-----------------------------------------------------------------------" Write-host "Runtimes for $arch" Write-host "Local $archfolder" Write-host "Remote $archurl" if ($platform_default -ne "") { Write-host "server default for ${arch}: $platform_default" } if ($activename -ne "") { #the default names an immutable -r ARTIFACT while the active #is typically its materialized WORKING name - the beside-toml's #'name' field records which artifact the working copy came from, #so match on either identity $active_artifact = "" $activetoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $activename) + ".toml") if (Test-Path -Path $activetoml -PathType Leaf) { foreach ($tline in (Get-Content -Path $activetoml)) { $m = [regex]::Match($tline, '^\s*name\s*=\s*"(.*)"\s*$') if ($m.Success) { $active_artifact = $m.Groups[1].Value; break } } } $matchnote = "" if ($platform_default -ne "" -and ($activename -eq $platform_default -or $active_artifact -eq $platform_default)) { $matchnote = " (= server default)" } Write-host "active (local): $activename$matchnote" } Write-host "-----------------------------------------------------------------------" Write-host " Local Remote" Write-host "-----------------------------------------------------------------------" # 12345678910234567892023456789302345 $G = "`e[32m" #Green $Y = "`e[33m" #Yellow $R = "`e[31m" #Red $RST = "`e[m" #explicit ORDINAL sort: Hashtable key enumeration order is #undefined (and differs between the powershell editions), and #culture-sensitive Sort-Object collates differently too (NLS vs #ICU) - ordinal agrees across editions and with bash LC_ALL=C $localkeys = @($localdict.Keys) [array]::Sort($localkeys, [System.StringComparer]::Ordinal) foreach ($key in $localkeys) { $local_sha1 = $($localdict[$key]) if ($remotedict.ContainsKey($key)) { if ($local_sha1 -eq $remotedict[$key]) { $rhs = "Same version" $C = $G } else { $rhs = "UPDATE AVAILABLE" $C = $Y } } else { $C = $R $rhs = "(not listed on server)" } #ansi problems from cmd.exe not in windows terminal - review $C = "" $RST = "" $mark = " " if ($key -eq $activename) { $mark = "* " } $annot = "" if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } $lhs = "$key".PadRight(35, ' ') write-host -nonewline "${mark}${C}${lhs}${RST}" write-host "$rhs$annot" } $lhs_missing = "-".PadRight(35, ' ') $remotekeys = @($remotedict.Keys) [array]::Sort($remotekeys, [System.StringComparer]::Ordinal) foreach ($key in $remotekeys) { if (-not ($localdict.ContainsKey($key))) { #skip support files riding in the server's sha1sums beside #the runtimes (artifact metadata tomls etc) - same extension #set the local candidate filter excludes $rext = [System.IO.Path]::GetExtension($key) if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue } $annot = "" if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } write-host -nonewline " $lhs_missing" write-host "$key$annot" } } Write-host "-----------------------------------------------------------------------" Write-host "* = active (local selection)" } else { if (test-path -Path $archfolder -Type Container) { Write-host "-----------------------------------------------------------------------" Write-Host "Local runtimes for $arch" $dircontents = Get-PunkRuntimeCandidates $archfolder $activename = Get-PunkActiveRuntime $archfolder write-host "$(${dircontents}.count) runtime(s) in $archfolder" Write-host "-----------------------------------------------------------------------" foreach ($f in $dircontents) { $meta = Get-PunkRuntimeMetadataSummary $archfolder $f.Name $arch $lhs = "$($f.Name)".PadRight(35, ' ') if ($f.Name -eq $activename) { write-host "* $lhs (active) $meta" } else { write-host " $lhs $meta" } } Write-host "-----------------------------------------------------------------------" if ($activename -ne "" -and -not ($dircontents | Where-Object Name -eq $activename)) { write-host "WARNING: active runtime '$activename' (from active.toml) is not present - use 'punk-runtime.cmd use ' to reselect" } Write-host "Use: 'list -remote' to compare local runtimes with those available on the artifact server" Write-host "Use: 'use ' to select the runtime that 'run' launches" Write-host "Use: 'use -name>' to materialize an immutable -r artifact into its working name and select it" } else { write-host "No runtimes seem to be installed for $arch in $archfolder`nPlease use 'punk-runtime.cmd fetch' to install." write-host "Use 'punk-runtime.cmd list -remote' to see available runtimes for $arch" } } } 'platforms' { #enumerate platform folders: local (bin/runtime/*) and, with #-remote, the server's platforms.txt discovery manifest (raw-file #servers have no directory listing - the manifest is part of the #punkbin layout contract; third-party mirrors carry the same file) $localdirs = @() if (Test-Path -Path $rtfolder -PathType Container) { $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name) [array]::Sort($localdirs, [System.StringComparer]::Ordinal) } if ( $PSBoundParameters.ContainsKey('remote') ) { $manifesturl = "$artifacturl/platforms.txt" $manifestlocal = Join-Path -Path $rtfolder -ChildPath "platforms.txt" if (-not (Test-Path -Path $rtfolder -PathType Container)) { new-item -Path $rtfolder -ItemType Directory -force | out-null } try { Invoke-WebRequest -Uri $manifesturl -OutFile $manifestlocal -ErrorAction Stop Write-Host "Fetched $manifesturl" } catch { if (Test-Path -Path $manifestlocal -PathType Leaf) { Write-Host "WARNING: could not fetch ${manifesturl}: $($_.Exception.Message)" Write-Host "WARNING: using cached copy at $manifestlocal" } else { Write-Host "No platforms.txt available from ${manifesturl}: $($_.Exception.Message)" Write-Host "(pre-convention punkbin or third-party mirror without the discovery manifest)" Write-Host "Name platforms explicitly with -platform; canonical names: 'help platforms' in the punk shell" exit 1 } } $remoteplatforms = @() foreach ($line in (Get-Content -Path $manifestlocal)) { $line = $line.Trim() if ($line -eq "" -or $line.StartsWith("#")) { continue } $remoteplatforms += $line } Write-host "-----------------------------------------------------------------------" Write-Host "Platforms served by $artifacturl" Write-host "-----------------------------------------------------------------------" foreach ($p in $remoteplatforms) { $marks = @() if ($p -eq $script:PunkLocalPlatform) { $marks += "local platform" } if ($localdirs -contains $p) { $marks += "local dir present" } $lhs = "$p".PadRight(25, ' ') if ($marks.Count -gt 0) { write-host " $lhs ($($marks -join ', '))" } else { write-host " $lhs" } } foreach ($d in $localdirs) { if (-not ($remoteplatforms -contains $d)) { $lhs = "$d".PadRight(25, ' ') write-host " $lhs (local dir only - not served remotely)" } } Write-host "-----------------------------------------------------------------------" Write-Host "Use: 'list -remote -platform ' to see a platform's runtimes" } else { Write-host "-----------------------------------------------------------------------" Write-Host "Local platform folders under $rtfolder" Write-host "-----------------------------------------------------------------------" if ($localdirs.Count -eq 0) { write-host " (none - 'fetch' creates the local platform's folder)" } foreach ($d in $localdirs) { $lhs = "$d".PadRight(25, ' ') if ($d -eq $script:PunkLocalPlatform) { write-host "* $lhs (local platform)" } else { write-host " $lhs" } } Write-host "-----------------------------------------------------------------------" Write-Host "Use: 'platforms -remote' to see platforms served by the artifact server" Write-Host "Canonical platform names: 'help platforms' in the punk shell" } } 'help' { Show-PunkRuntimeHelp } default { #no action given (unknown actions were already rejected in the #process block): show the short usage and point at 'help' Show-PunkRuntimeUsage write-host "" write-host "'punk-runtime.cmd help' gives options, env vars and examples." } } return $PSBoundParameters } } #write-host (psmain @args) #$returnvalue = psmain @args #Write-Host "Function Returned $returnvalue" -ForegroundColor Cyan #return $returnvalue psmain @args | out-null exit 0 # # -- --- --- --- --- --- --- --- --- --- --- --- --- ---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) #>