I have a PowerShell script that I converted to an EXE with Win-PS2EXE.The problem is that I'm using Foreach -parallel
and it doesn't work with PowerShell 5.0, I need this EXE to run with the latest shell and i already tried using #Requires -Version 7.0
and it stills running with the old one.
Tried with: #Requires -Version 7.0 #Requires -PSEdition Desktop
PS2EXE
as well as its GUI front-end, Win-PS2EXE
, are designed to work only with Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1), and not also with PowerShell (Core) 7 (the modern, cross-platform, install-on-demand edition), at least as of this writing.
The compiled executables require a compatible Windows PowerShell version to be present at runtime, because the executable isn't fully self-contained.
In that sense, building on Windows PowerShell only has one advantage: The latter is guaranteed to be present on target machines, unlike PowerShell (Core) 7.
If you can assume the presence of PowerShell (Core) 7 on all machines where your executable must run, there is an - inefficient - workaround:
From the script you're compiling, make a call to pwsh.exe
, the PowerShell (Core) 7 CLI, passing the code to execute via a script block, as shown below.
The sample script below assumes that pwsh.exe
is discoverable via $env:PATH
, and therefore uses the file name only.
If this is not the case or you want to hard-code the executable's path for predictability, use the full path, and invoke it via the call operator; e.g., to launch a winget
-installed copy:
& $env:LOCALAPPDATA\Microsoft\WindowsApps\pwsh.exe ...
Note that even though execution of a script file is not involved, CLI parameter -ExecutionPolicy Bypass
is passed in order to unconditionally enable execution script execution, because execution of files subject to the effective execution policies may still be involved indirectly, given that not only *.ps1
files, but also script modules (*.psm1
) as well as formatting- and type-definition files are subject to the execution policy.
However, note that a GPO (Group Policy Object)-mandated execution policy may override this.
See this answer for details.
-NoProfile
is passed in order to suppress profile loading, which avoids unnecessary processing and makes for a more predictable execution environment.
Caveat:
*.ps1
file is large, passing its content directly on the command line can fail; see the bottom section for a file-based workaround.Sample source-code script:
# Content of a sample *.ps1 file to be compiled to an *.exe file
# with Invoke-ps2exe
# Sample parameter declarations.
param(
[string] $Foo,
[int] $Bar
)
# Not running in PowerShell (Core) 7? Re-invoke with the latter's CLI.
if (-not $IsCoreCLR) {
# Create a helper script block that incorporates this script's,
# to facilitate passing arguments through faithfully.
$scriptBlock = [scriptblock]::Create(@"
param(`$bound, `$undeclared)
. { $($MyInvocation.MyCommand.ScriptBlock) } @bound @undeclared
"@)
pwsh.exe -ExecutionPolicy Bypass -NoProfile $scriptBlock -args $PSBoundParameters, (@(), $args)[$null -ne $args]
# Exit and pass pwsh.exe's exit code through.
exit $LASTEXITCODE
}
# Getting here means that the code is run by PowerShell (Core) 7 now.
# == Place your PowerShell (Core) 7 code here. ==
# Any arguments originally passed to your *.exe file
# have been passed through, irrespective of whether the were bound
# to formally declared parameters or anonymously positionally.
# Sample command that uses ForEach-Object -Parallel
1..3 | ForEach-Object -Parallel {
"ID of thread #${_}: " + [System.Threading.Thread]::CurrentThread.ManagedThreadId
}
Note:
The if (-not $IsCoreCLR) { ... }
statement is generic, reusable code that you can incorporate into any script compiled to an .exe
file via Invoke-ps2exe
:
It re-invokes the script's code via pwsh.exe
, the PowerShell (Core) 7 CLI, and exits right after, passing the latter's output and exit code through.
$IsCoreCLR
variable, which is only $true
(and only defined) in PowerShell (Core) 7, is used to detect whether the script code is being run in Windows PowerShell or PowerShell (Core) 7; another option would be to use $PSVersionTable.PSEdition -ne 'Core'
A helper script block is created that wraps the script's own script block (obtainable via $MyInvocation.MyCommand.ScriptBlock
) in order to facilitate passing any arguments received through to the re-invocation via pwsh.exe
, by way of the automatic $PSBoundParameters
variable and the automatic $args
variable, using splatting to pass the arguments to the original script code.
The filename or extension is too long
:The implication is that the source-code string length of your original script file resulted in the pwsh.exe
command line exceeding the system's max. command-line length.
The solution is to write the source code to a temporary *.ps1
file first, and then make the pwsh.exe
invoke that, as shown below.
# Content of a sample *.ps1 file to be compiled to an *.exe file
# with Invoke-ps2exe
# Sample parameter declarations.
param(
[string] $Foo,
[int] $Bar
)
# Not running in PowerShell (Core) 7? Re-invoke with the latter's CLI.
if (-not $IsCoreCLR) {
# Create a temp. *.ps1 file and save this script's source code to it.
# Note: NO *.ps1 file is involved in a ps2exe-compiled script; instead
# the source code is embedded in the *.exe file.
$tmpScriptFile = (New-TemporaryFile).FullName
Remove-Item -LiteralPath $tmpScriptFile; $tmpScriptFile += '.ps1'
$MyInvocation.MyCommand.ScriptBlock | Set-Content -Encoding utf8 $tmpScriptFile
# Now invoke the temp. *.ps1 file, passing all arguments through.
pwsh.exe -ExecutionPolicy Bypass -NoProfile { param($scriptFile, $bound, $undeclared) . $scriptFile @bound @undeclared } -args $tmpScriptFile, $PSBoundParameters, (@(), $args)[$null -ne $args]
Remove-Item -LiteralPath $tmpScriptFile # clean up
# Exit and pass pwsh.exe's exit code through.
exit $LASTEXITCODE
}
# Getting here means that the code is run by PowerShell (Core) 7 now.
# == Place your PowerShell (Core) 7 code here. ==
# Any arguments originally passed to your *.exe file
# have been passed through, irrespective of whether the were bound
# to formally declared parameters or anonymously positionally.
# Sample command that uses ForEach-Object -Parallel
1..3 | ForEach-Object -Parallel {
"ID of thread #${_}: " + [System.Threading.Thread]::CurrentThread.ManagedThreadId
}