You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
957 lines
53 KiB
957 lines
53 KiB
|
|
|
|
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 ?<name>? ?-platform <p>? download+verify a runtime from punkbin" |
|
write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata" |
|
write-host " use <name> ?-platform <p>? select active / materialize -r<N> 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/<platform>/" |
|
write-host "" |
|
Show-PunkRuntimeUsage |
|
write-host "" |
|
write-host "Actions:" |
|
write-host " fetch ?<name>? ?-platform <p>?" |
|
write-host " Download a runtime from the punkbin artifact server into" |
|
write-host " bin/runtime/<p>/ with sha1 verification against the server's" |
|
write-host " sha1sums.txt. -r<N>-named family artifacts also fetch their" |
|
write-host " <rootname>.toml metadata record. Omitting <name> 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 " <name>." |
|
write-host " list ?-remote? ?-platform <p>?" |
|
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 <name> ?-platform <p>?" |
|
write-host " Select the runtime that 'run' launches (records active.toml in" |
|
write-host " the platform folder). An immutable -r<N> ARTIFACT name is" |
|
write-host " MATERIALIZED into its WORKING name (name minus -r<N> - 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 <rootname>.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 <runtimename> ?-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 <runtimename> ?-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<N> 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 <runtimename> ?-platform <p>?" |
|
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<N>, immutable): 'use' MATERIALIZES the |
|
#artifact into its WORKING name (name minus -r<N> - 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 <name> (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 <name>" |
|
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 "") { |
|
$matchnote = "" |
|
if ($platform_default -ne "" -and $activename -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))) { |
|
$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 <name>' to reselect" |
|
} |
|
Write-host "Use: 'list -remote' to compare local runtimes with those available on the artifact server" |
|
Write-host "Use: 'use <name>' to select the runtime that 'run' launches" |
|
Write-host "Use: 'use <artifact-r<N>-name>' to materialize an immutable -r<N> 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 <name>' 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 |
|
|
|
|