Caution
Do not store passwords in source code.
See Password security for details.
ps12exe is a PowerShell module that lets you create an executable file from a .ps1 script.
Install-Module ps12exe # Install the ps12exe module
Set-ps12exeIntegration # Set up the right-click menu, Agent Skill and VS Code extension(You can also clone this repository and run ./ps12exe.ps1 directly.)
Upgrading from PS2EXE to ps12exe? No problem!
PS2EXE2ps12exe redirects PS2EXE calls to ps12exe. Uninstall PS2EXE, install this module, then use PS2EXE as usual.
It aims for maximum compatibility with every PS2EXE version (including conHost, embedFiles and legacy runtime20/runtime40); capabilities ps12exe lacks are rewritten at compile time.
Uninstall-Module PS2EXE
Install-Module PS2EXE2ps12exeOnce you've run Set-ps12exeIntegration, you can right-click any ps1 file to quickly compile it into an exe or open ps12exeGUI with that file.

ps12exeGUIps12exe .\source.ps1 .\target.exeCompiles source.ps1 into the executable target.exe (if .\target.exe is omitted, output goes to .\source.exe).
'"Hello World!"' | ps12exeCompiles "Hello World!" into the executable .\a.exe.
ps12exe https://raw.githubusercontent.com/steve02081504/ps12exe/master/src/GUI/Main.ps1Compiles Main.ps1 from the internet into the executable .\Main.exe.
exe21sp -inputFile .\target.exe -outputFile .\target.ps1exe21sp extracts the PowerShell script embedded in a ps12exe-generated executable and saves it as a .ps1 file or prints it to standard output. It uses the same $LastExitCode convention as ps12exe: 0 = success, 1 = input/parse error (e.g. not a ps12exe exe), 2 = invocation error (e.g. no input when redirected), 3 = resource/internal error (e.g. file not found).
- ps12exe: When stdout (or stdin/stderr) is redirected, ps12exe writes only the path of the generated exe to stdout so you can capture it (e.g.
$exe = ps12exe .\a.ps1). - exe21sp: Accepts exe paths or URLs from pipeline input (e.g.
Get-ChildItem *.exe | exe21spor".\app.exe" | exe21sp). - exe21sp: If
-outputFileis not specified and stdout is not redirected, the decompiled script is saved to a.ps1file with the same base name as the exe in the same directory. - exe21sp: If
-outputFileis not specified and stdout is redirected, the decompiled script is written to stdout.
Start-ps12exeWebServerStarts a web server that lets users compile PowerShell code online.
The ps12exe VS Code extension compiles a .ps1 script into an executable — or opens ps12exeGUI — without leaving the editor, and adds editor support for the preprocessor directives (syntax highlighting, diagnostics, #_if auto-close, folding, go-to-definition, hover, completion and formatting).
Set-ps12exeIntegration installs it automatically; you can also install steve02081504.ps12exe manually.
Set-ps12exeIntegration also writes a ps12exe Agent Skill to ~/.agents/skills, so skill-aware coding agents (opencode, Codex, Cursor, GitHub Copilot, …) know to use ps12exe when asked to compile a PowerShell script into an executable. Set-ps12exeIntegration -action disable removes it, and Set-ps12exeIntegration -Skip AgentSkill skips it.
ps12exeGUI [[-ConfigFile] '<config file>'] [-PS1File '<PS1 file>'] [-Locale '<language code>'] [-UIMode 'Dark'|'Light'|'Auto'] [-help]
ps12exeGUI [[-PS1File] '<PS1 file>'] [-Locale '<language code>'] [-UIMode 'Dark'|'Light'|'Auto'] [-help]ConfigFile : Configuration file to load.
PS1File : Script file to compile.
Locale : Language code to use.
UIMode : UI mode (Dark, Light, or Auto).
help : Shows this help message.
[input |] ps12exe [[-inputFile] '<filename|url>' | -Content '<script>'] [-outputFile '<filename>']
[-App @{Windowed=$true; Silence=@('Output','Error'); OutputEncoding='UTF8'|'UTF16LE'|'Default'; VisualStyles=$true; DarkMode='Auto'|'On'|'Off';
ExitOnCancel=$true; CredentialGUI=$true; DpiAware=$true; WinFormsDpiAware=$true; ConHost=$true}]
[-Os @{Admin=$true; ModernOS=$true; LongPaths=$true; Virtualize=$true}]
[-Build @{Target='Framework4.0'|'Framework2.0'|'Core'; Platform='AnyCpu'|'x64'|'x86'|'arm64'; Apartment='STA'|'MTA';
Culture='<culture>'; Options='<options>'; KeepSource=$true; Minify={<scriptblock>}; TempDir='<directory>';
Core=@{Backend='Shared'|'Bundled'; TargetOs='Windows'|'Linux'|'MacOS'; TargetFramework='<net8.0>'; PowerShellVersion='<version>'; SingleFile=$true; SelfContained=$true; Trimmed=$true; TrimMode='partial'|'full'; ReadyToRun=$true; InvariantGlobalization=$true; Aot=$true};
DllExports=@(@{funcName='<function name>'; returnType='<type>'; params=@(@{type='<type>';name='<param name>'})})}]
[-Resources @{Icon='<file|url>'; Title='<title>'; Description='<description>'; Company='<company>';
Product='<product>'; Copyright='<copyright>'; Trademark='<trademark>'; Version='<version>'}]
[-Signing @{Certificate='<PFX path>'; Password='<password>'; Thumbprint='<thumbprint>'; Timestamp='<timestamp server>'}]
[-PreprocessOnly] [-Golf] [-Sandbox] [-NoUpdateCheck] [-Quiet] [-Locale '<language code>'] [-ConfigFile] [-help]input : String contents of the PowerShell script (same as -Content).
inputFile : Path or URL of the PowerShell script to convert (file must be UTF-8 or UTF-16 encoded).
Content : PowerShell script text to convert to an executable.
outputFile : Output executable path or folder; defaults to the input file path with a `.exe` extension.
App : A hashtable describing how the produced application behaves. Supported keys:
Windowed : Build a Windows Forms application without a console window.
Silence : Stream names to suppress; one or more of 'Output', 'Verbose', 'Error', 'Warning', 'Debug', or '*'.
OutputEncoding : Console output encoding; 'Default', 'UTF8' or 'UTF16LE'.
VisualStyles : Enable visual styles for GUI applications (default $true).
DarkMode : Dark theme for windowed GUI WinForms windows; 'Auto' follows the system theme, 'On'/'Off' force it.
ExitOnCancel : Exit when Cancel or 'X' is selected in a Read-Host input box.
CredentialGUI : Use a GUI for prompting credentials in console mode.
DpiAware : Mark the compiled executable as DPI aware.
WinFormsDpiAware : Let WinForms use DPI scaling (requires Windows 10 and .NET 4.7 or up).
ConHost : Force a conhost console instead of Windows Terminal; disables input/output/error redirection.
Os : A hashtable of OS integration options. Supported keys:
Admin : If UAC is enabled, the compiled executable runs only in an elevated context (UAC dialog appears).
ModernOS : Use functions of the newest Windows versions (run [Environment]::OSVersion to see the difference).
LongPaths : Enable long paths ( > 260 characters) if enabled on the OS (Windows 10 or up).
Virtualize : App virtualization is activated (forcing x86 runtime).
Build : A hashtable of build options. Supported keys:
Target : Target runtime version, 'Framework4.0' by default; 'Framework2.0' and 'Core' are supported. 'Core' builds a PowerShell Core (.NET) executable (needs PowerShell Core and .NET on both build and target machines; the output is much larger).
Platform : Compile for a specific runtime. Possible values are 'AnyCpu', 'x64', 'x86' and 'arm64' (arm64 only for 'Core').
Apartment : 'STA' (Single Thread Apartment) or 'MTA' (Multi Thread Apartment) mode.
Culture : Locale for the compiled executable. Uses the current user culture if omitted.
Options : Additional compiler options (see https://msdn.microsoft.com/en-us/library/78f4aasd.aspx).
KeepSource : Creates info to help with debugging.
Minify : Scriptblock to minify the script before compiling.
TempDir : Directory for temporary files (default: a random folder under %temp%).
Core : Options for 'Core' target builds. Supported keys:
Backend : 'Shared' (default) resolves PowerShell from the target machine's pwsh install and keeps the output small; 'Bundled' bundles the PowerShell SDK (Microsoft.PowerShell.SDK) so the target machine needs no pwsh and SelfContained/Trimmed/ReadyToRun/InvariantGlobalization/Aot become available, at the cost of a much larger output.
TargetOs : Target operating system: 'Windows', 'Linux' or 'MacOS' (default: the build machine's OS). GUI/windowed output requires 'Windows'.
TargetFramework : Target .NET framework moniker (for example 'net8.0'). Defaults to the build machine's runtime (Shared) or the framework mapped to PowerShellVersion (Bundled).
PowerShellVersion : Version of the bundled PowerShell SDK (Bundled only). Defaults to the build machine's PowerShell version.
SingleFile : Publish a single-file executable (default $true). When $false, the executable and its dependencies are written as a folder.
SelfContained : Include the .NET runtime (Bundled only; default $false). Much larger but needs no installed runtime.
Trimmed : Enable IL trimming to reduce size (Bundled only; default $false).
TrimMode : Trimming aggressiveness when Trimmed is set: 'partial' (default, safe) or 'full' (aggressive, may break reflection).
ReadyToRun : Pre-compile assemblies for faster startup (Bundled only).
InvariantGlobalization : Use invariant globalization, removing ICU libraries from self-contained builds (Bundled only).
Aot : Experimental Native AOT compilation (Bundled only; requires SelfContained). Heavy reflection used by PowerShell may break some scripts.
DllExports : Native DLL export array (equivalent to #_DllExport in the script): each item is @{funcName=...; returnType=...; params=@(@{type=...;name=...})}. Only Framework4.0 plus x86/x64; AnyCPU picks the bitness automatically and warns; arm64/Framework2.0/Core error out. Disabled in Sandbox mode.
Resources : A hashtable of version resources embedded into the executable (Icon, Title, Description, Company, Product, Copyright, Trademark, Version). Icon can be a file path or URL; for .exe/.dll append ,<index> to pick a resource icon (default 0), e.g. shell32.dll,3.
Signing : A hashtable of code signing options (Certificate, Password, Thumbprint, Timestamp). Either Certificate or Thumbprint must be specified.
PreprocessOnly : Preprocess the input script and return it without compiling.
Golf : Enable golf mode, adding abbreviations and common functions.
Sandbox : Compile scripts with extra protection, preventing access to native files.
NoUpdateCheck : Skip the check for new versions of ps12exe.
Quiet : Suppress informational (host) output during compilation; errors and warnings are still shown.
Locale : Language code for localized messages.
ConfigFile : Write a config file (<outputfile>.exe.config).
Help : Show this help message.
Unlike many PowerShell functions, ps12exe sets $LastExitCode to report success or failure, but it may still throw exceptions.
Check the exit code like this:
$LastExitCodeBackup = $LastExitCode
try {
'"some code!"' | ps12exe
if ($LastExitCode -ne 0) {
throw "ps12exe failed with exit code $LastExitCode"
}
}
finally {
$LastExitCode = $LastExitCodeBackup
}$LastExitCode meanings:
| Error type | $LastExitCode |
|---|---|
| 0 | Success |
| 1 | Input/code error |
| 2 | Invocation error |
| 3 | Internal ps12exe error |
ps12exe pre-processes the script before compiling.
# Read the program frame from the ps12exe.cs file
#_if PSEXE # This is the preprocessing code used when the script is compiled by ps12exe
#_include_as_value programFrame "$PSScriptRoot/ps12exe.cs" # Inline the contents of ps12exe.cs into this script
#_else # Otherwise read the cs file normally
[string]$programFrame = Get-Content $PSScriptRoot/ps12exe.cs -Raw -Encoding UTF8
#_endif$LocalizeData =
#_if PSScript
. $PSScriptRoot\src\LocaleLoader.ps1
#_else
#_include "$PSScriptRoot/src/locale/en-UK.psd1"
#_endifRight now, only these conditions are supported: PSEXE and PSScript.
PSEXE is true; PSScript is false.
#_include <filename|url>
#_include_as_value <valuename> <file|url>Includes the content of the file <filename|url> or <file|url> into the script. The file's content is inserted where the #_include/#_include_as_value command is.
Unlike the #_if statement, if you don't put quotes around the filename, the #_include commands treat trailing spaces and # as part of the filename as well.
#_include $PSScriptRoot/super #weird filename.ps1
#_include "$PSScriptRoot/filename.ps1" #safe comment!The file's content is preprocessed when #_include is used, letting you include files at multiple levels.
#_include_as_value inserts the file's content as a string value into the script. The file's content isn't preprocessed.
Most of the time, you don't need to use #_if and #_include to make scripts include sub-scripts correctly after converting to an exe. ps12exe automatically handles stuff like this and assumes the target script should be included:
. $PSScriptRoot/another.ps1
& $PSScriptRoot/another.ps1
$result = & "$PSScriptRoot/another.ps1" -args#_include_as_base64 <valuename> <file|url>
#_include_as_bytes <valuename> <file|url>Includes a file's content as a base64 string or byte array at preprocessing time. The file content itself isn't preprocessed.
Here's a simple packer example:
#_include_as_bytes mydata $PSScriptRoot/data.bin
[System.IO.File]::WriteAllBytes("data.bin", $mydata)When run, this EXE extracts the data.bin file that was embedded in the script during compilation.
$Script:eshDir =
#_if PSScript # It's not possible to have $EshellUI in PSEXE
if (Test-Path "$($EshellUI.Sources.Path)/path/esh") { $EshellUI.Sources.Path }
elseif (Test-Path $PSScriptRoot/../path/esh) { "$PSScriptRoot/.." }
elseif
#_else
#_!!if
#_endif
(Test-Path $env:LOCALAPPDATA/esh) { "$env:LOCALAPPDATA/esh" }The #_!! prefix is stripped from any line that starts with it.
#_require ps12exe
#_pragma App.Windowed
$Number = [bigint]::Parse('0')
$NextNumber = $Number+1
$NextScript = $PSEXEscript.Replace("Parse('$Number')", "Parse('$NextNumber')")
$NextScript | ps12exe -outputFile $PSScriptRoot/$NextNumber.exe *> $null
$Number#_require counts the modules needed in the whole script and adds a script equivalent to this before the first #_require:
$modules | ForEach-Object{
if(!(Get-Module $_ -ListAvailable -ea SilentlyContinue)) {
Install-Module $_ -Scope CurrentUser -Force -ea Stop
}
}Note that the generated code only installs modules, it doesn't import them.
Use Import-Module as needed.
When you need several modules, you can separate them with spaces, commas, or semicolons instead of multiple #_require lines.
#_require module1 module2;module3、module4,module5The pragma preprocessor directive doesn't change the script's content, but it changes the parameters used for compilation. Here's an example:
PS C:\Users\steve02081504> '12' | ps12exe
Compiled file written -> 1024 bytes
PS C:\Users\steve02081504> ./a.exe
12
PS C:\Users\steve02081504> '#_pragma App.Windowed
>> 12' | ps12exe
Preprocessed script -> 23 bytes
Compiled file written -> 2560 bytesAs you can see, #_pragma App.Windowed makes the generated exe file run in windowed mode, even if we didn't specify -App @{Windowed=$true} during compilation.
The pragma command can set any compilation parameter; use . in the name to set nested values:
#_pragma App.Windowed # windowed
#_pragma App.Windowed $false # console
#_pragma Resources.Icon $PSScriptRoot/icon.ico # set icon
#_pragma Resources.Title "title" # set title
#_pragma Signing.Certificate "C:\Cert\mycert.pfx" # set the code signing certificateString pragma values can also contain $(...) subexpressions, which are evaluated at preprocess time, e.g. #_pragma Resources.Icon $(Join-Path $env:USERPROFILE 'foo.ico'). Only whitelisted path-related commands (Get-Command, Join-Path, Split-Path, Resolve-Path, Convert-Path, Get-Item, Test-Path, Get-ChildItem, plus Get-Content outside Sandbox), variables ($env:* (inside Sandbox only $env:windir/$env:SystemRoot), $PSScriptRoot, $ScriptRoot, $HOME, $PWD, $PSCommandPath) and common harmless instance methods (e.g. ToUpper, Trim, Split, ToString) are allowed; anything else aborts the compile. Single-quoted values stay fully literal. Sandbox mode also ignores #_pragma outputFile, Build.TempDir, Build.Minify, and Signing.Certificate, and only fetches http(s) URLs that resolve to public addresses (redirect targets are restricted the same way). Local Resources.Icon paths are only allowed under the Windows directory.
#_DllExport int Add(int a, int b)
#_DllExport Add(int a, int b)
#_DllExport DoSomething(int value)
function Add($a, $b) { return $a + $b }
function DoSomething($value) { ... }#_DllExport compiles the script into a native Win32 DLL instead of an executable, exporting the listed functions so native callers can use LoadLibrary/GetProcAddress (or DllImport) directly. Each exported function forwards to the PowerShell function of the same name: arguments are passed as an array and the function's output becomes the return value. Return and parameter types are written in C# syntax; an omitted return type means void, and a parameter without a type is treated as string.
Native exports require a .NET Framework 4.0 target and an x86/x64 platform (AnyCPU is resolved automatically to the host bitness), and the output defaults to .dll. The feature is unavailable in guest (sandbox) mode. Calls are serialized, and the first one starts the PowerShell runspace. A script error is written to stderr and the call returns the declared type's default value rather than let the exception cross the native boundary.
#_balus <exitcode>
#_balusWhen the code gets here, the process exits with the given exit code and deletes the EXE.
Since ps12exe's "compilation" embeds everything in the script as a resource in the resulting executable, the executable will be large if the script has a lot of useless strings.
You can use the Minify key of -Build to specify a script block that processes the script after preprocessing but before compilation to get a smaller executable.
If you don't know how to write one of those script blocks, you can use psminnifyer.
& ./ps12exe.ps1 ./main.ps1 -App @{Windowed=$true} -Build @{Minify={ $_ | &./psminnifyer.ps1 }}The basic input/output commands had to be rewritten in C# for ps12exe. Not implemented: Write-Progress in console mode (prohibitively much work), and Start-Transcript / Stop-Transcript (no suitable reference implementation from Microsoft).
By default, PowerShell formats cmdlet output line by line (as a string array). If a command produces 10 lines and you use GUI output, you get 10 message boxes. Pipe to Out-String to combine lines into one string so a single message box shows everything (e.g. dir C:\ | Out-String).
ps12exe can create config files named after the generated executable + ".config". Most of the time, these aren't needed; they just say which .Net Framework version to use. Since you'll usually use the current .Net Framework, try running your executable without the config file first.
Compiled scripts handle parameters like the original script. One limitation is Windows itself: for any executable, command-line arguments are ultimately strings.
When the script has a top-level param() block, ps12exe parses argument values as PowerShell data (PSD): hashtables @{}, ordered hashtables [ordered]@{}, arrays @(), and casts to safe types such as [int]'5' or [hashtable]@{} are passed to the parameter as the corresponding object, so no manual conversion is needed:
# script: param([hashtable]$Config)
app.exe -Config "@{name='Bob'; tags=@('a','b')}"
app.exe -Config "[ordered]@{first='1'; second='2'}"The value must be quoted: shells stringify an unquoted @{...} (the program only receives the text System.Collections.Hashtable). Anything that is not data (an expression or a command) is passed as a plain string and is never evaluated, so -Config "@{x=(Get-Date)}" does not run code — it simply fails to bind.
Piped input values are still strings.
Never store passwords in your compiled script! The whole script is easy to read with any .NET decompiler.
You can tell if a script is running in a compiled exe or in a script by checking $Host.Name.
if ($Host.Name -eq "PSEXE") { Write-Output "ps12exe" } else { Write-Output "Some other host" }Since ps12exe turns a script into an executable, the variable $MyInvocation is set to different values than in a script.
You can still use $PSScriptRoot to get the directory where the executable is, and $PSCommandPath to get the path of the executable itself.
When an external window is opened in a script with App.Windowed mode (like for Get-Credential or a command that needs a cmd.exe shell), the next window opens in the background.
This happens because when the external window closes, Windows tries to activate the parent window. Since the compiled script has no window, the parent window of the compiled script gets activated instead, usually Explorer or PowerShell's window.
To fix this, $Host.UI.RawUI.FlushInputBuffer() opens an invisible window that can be activated. The following call of $Host.UI.RawUI.FlushInputBuffer() closes this window (and so on).
The following avoids the background-window issue, unlike a single ipconfig | Out-String alone:
$Host.UI.RawUI.FlushInputBuffer()
ipconfig | Out-String
$Host.UI.RawUI.FlushInputBuffer()A compiled exe only reads a redirected standard input (line by line, as pipeline input) when the script uses $input at its top level:
- If
$inputis used: behaves like PS2EXE — stdin is consumed as pipeline input, so raw stdin ([Console]::In/Console.OpenStandardInput()) reaches EOF afterwards. - If
$inputis not used: stdin is not read at all; the raw standard input is left untouched and startup does not wait for stdin (a parent that keeps the pipe open no longer blocks it), and child processes can still inherit stdin.
Only the script's top level counts: a $input inside functions, script blocks or classes is that scope's own pipeline input and is ignored.
For example, if the script calls [Console]::In.ReadToEnd() and never uses $input, then after compiling, echo hi | .\tool.exe receives hi.
For constant-only, side-effect-free scripts, ps12exe evaluates them at compile time and emits the result as a tiny exe (usually around 1KB); it falls back to the normal build when evaluation times out (7 seconds by default) or the result is too long. If the evaluation environment differs from runtime, or you simply want the full PowerShell host, add either pragma below to opt out of that optimization explicitly:
#_pragma Build.ConstEval.Enabled 0: declare this script is not a constant; skip constant evaluation.#_pragma Build.ConstEval.Timeout 1: declare constant evaluation already timed out; fall back as if it had timed out.
Both are parsed as ordinary nested pragmas during preprocessing, so they can go on any line; after falling back to the normal host they are just ordinary comments.
| Comparison Content | ps12exe | MScholtes/PS2EXE@1.0.18 |
|---|---|---|
| Pure script repository 📦 | ✔️ All text files except images & bundled helper DLLs | ❌ Ships Win-PS2EXE.exe with an open source license |
| Command to generate hello world 🌍 | 😎'"Hello World!"' | ps12exe |
🤔echo "Hello World!" *> a.ps1; PS2EXE a.ps1; rm a.ps1 |
| Constant hello world executable 💾 | 🥰1024 bytes (constant-evaluated at compile time) | ❌ Not supported; 25088 bytes |
| Non-constant hello world executable 💾 | 🥰13312 bytes | 😨25088 bytes |
| Compile-time constant evaluation ⚡ | ✔️ | ❌ |
| PowerShell Core (7+) / cross-platform target 🧬 | ✔️ Build.Target Core (Windows / Linux / macOS) |
❌ Windows PowerShell 5.1 only |
| GUI multilingual support 🌐 | ✔️ (7 languages, dark mode) | ❌ |
| Syntax check during compilation ✔️ | ✔️ | ❌ |
| Preprocessing feature 🔄 | ✔️ | ❌ |
-extract and other special parameter parsing 🧹 |
🗑️ Removed (use the exe21sp tool instead) |
🥲 Requires source code modification |
| PR welcome level 🤝 | 🥰 Welcome! | 🤷 14 PRs, 13 of which were closed |
| Political / DEI / stance bias 🕊️ | ✔️ None; any valuable PR is welcome — from a human, an AI, or a monkey at a typewriter | ❌ Readme takes an anti-AI stance ("artificial intelligence is killing creativity and our nature") |
ps12exe's developer does not use this project to promote a political, DEI or other ideological stance — any valuable PR is welcome, whether it comes from a human, an AI, or a monkey at a typewriter.
Measured on Windows 11 with PowerShell 7.6.6 (.NET 10) and Windows PowerShell 5.1 on 2026-10-01, 20 warm runs each. The process-creation floor (cmd /c exit) is ~15 ms. Reproduce with pwsh -File ../tools/Benchmark/Compare-Compilers.ps1 -IncludeCore (add -Compile for the compilation-speed table below).
| Build | Output size | Warm startup |
|---|---|---|
| Windows PowerShell 5.1 running the script directly | — | ~294 ms |
| ps12exe · constant · Framework4.0 | 1024 bytes | ~47 ms |
| ps12exe · non-constant · Framework4.0 | 13312 bytes | ~248 ms |
| PS2EXE 1.0.18 · non-constant | 25088 bytes | ~261 ms |
| ps12exe · non-constant · large script · Framework4.0 | 25088 bytes | ~293 ms |
| PS2EXE 1.0.18 · non-constant · large script | ~496 KB | ~275 ms |
| ---------------------------------------------------- | ----------- | ------------ |
| pwsh 7 running the script directly | — | ~461 ms |
| ps12exe · constant · Core | ~165 KB | ~86 ms |
| ps12exe · non-constant · Core | ~180 KB | ~370 ms |
| ps12exe · non-constant · large script · Core | ~188 KB | ~401 ms |
| PS2EXE 1.0.18 · non-constant · Core | not support | not support |
A constant script is evaluated at compile time, so its exe is 1 KB and never starts PowerShell — about 24× smaller and 6× faster to launch than a PS2EXE hello world. Non-constant exes are smaller than PS2EXE's in these measurements, and for top-level-variable-heavy scripts they can also run faster because the script executes inside a function (local scope) rather than at global scope. Non-constant exes are always compressed: the ~482 KB test script produces a 25 KB Framework exe, while PS2EXE produces ~496 KB. The Core exe adds only ~8 KB over its small-script counterpart.
The hello-world benchmark above measures the launch of a console app that exits immediately, so its numbers are dominated by process startup. A GUI app stays alive while the user interacts, so the meaningful figure there is the time between double-clicking and the window appearing — the startup and script-execution overhead, not the process lifetime. Measured with the same tool (-Windowed), using a WinForms window that closes itself after ~800 ms; each row's warm startup includes process startup and script execution (the window lifetime is nearly identical across rows, and the ~15 ms process-creation floor is negligible here).
| Build | Output size | Warm startup |
|---|---|---|
| Windows PowerShell 5.1 running the script directly (hidden window) | — | ~1154 ms |
| ps12exe · windowed · DarkMode Auto · Framework4.0 | 29184 bytes | ~1128 ms |
| PS2EXE 1.0.18 · windowed · non-constant | 33792 bytes | ~1128 ms |
| ------------------------------------------------------------------ | ----------- | ------------ |
| pwsh 7 running the script directly (hidden window) | — | ~1349 ms |
| ps12exe · windowed · DarkMode Auto · Core | ~6385 KB | ~1317 ms |
A windowed ps12exe app starts about as fast as PS2EXE's (~1128 ms), and both are a hair faster than running the same script through PowerShell directly — ps12exe strips -NoProfile-style overhead and never re-parses, while still adding a Silence/OutputEncoding host wrapper and dark-mode support. The Core windowed app (~1317 ms) is only slightly slower than the Framework one and a bit faster than pwsh running the script directly (~1349 ms). The ~6.4 MB Core output is self-contained by default; disable it with -Build @{Target='Core'; SelfContained=$false} to fall back to the ~0.2 MB shared-runtime exe. The size gain of the Framework windowed app over PS2EXE's (~4.5 KB) is smaller than for console apps because both must embed the WinForms bootstrap.
Measured with the same tool (-Compile -IncludeCore). Each sample is a fresh host process (Windows PowerShell 5.1 for Framework/PS2EXE, pwsh 7 for Core); "warm" is the median of 5 compiles after the first. PS2EXE numbers use the locally installed release (1.0.18 in this environment).
| Build | Warm compile |
|---|---|
| ps12exe · constant · Framework4.0 | ~1466 ms |
| ps12exe · non-constant · Framework4.0 | ~900 ms |
| PS2EXE · non-constant | ~585 ms |
| ps12exe · non-constant · large script · Framework4.0 | ~2063 ms |
| PS2EXE · non-constant · large script | ~596 ms |
| ---------------------------------------------------- | ------------ |
| ps12exe · constant · Core | ~2742 ms |
| ps12exe · non-constant · Core | ~3633 ms |
| ps12exe · non-constant · large script · Core | ~5402 ms |
| PS2EXE · non-constant · Core | not support |
PS2EXE compiles a hello world faster because it is a thin wrapper around the .NET Framework compiler built into Windows: it performs a single CodeDom pass and nothing else. ps12exe additionally runs a syntax check, classifies the script and (for constant scripts) evaluates it, and packs the program frame as a payload inside a launcher, making its non-constant hello-world compile ~1.5× PS2EXE's in this run. ps12exe emits 1024 / 13312 bytes where PS2EXE emits 25088, and constant programs launch about 6× faster. Core compilation is dominated by dotnet publish; the first compile for a given configuration also restores NuGet packages, after which ps12exe reuses the generated project directory and runs dotnet publish --no-restore.
The compiler itself is installed as a PowerShell module:
| Compiler package | Unpacked | Compressed |
|---|---|---|
| ps12exe (current working-tree package) | 1,899,082 bytes | 647,312 bytes |
| PS2EXE 1.0.18 (installed files) | 169,382 bytes | 44,082 bytes |
ps12exe's module is larger because it is a dependency-free, pure-script compiler that bundles trimmed AsmResolver binaries, 7 localizations and a pure-script GUI; PS2EXE ships almost nothing and relies on the .NET Framework compiler built into Windows.
A script with #_DllExport is compiled into a Win32 DLL callable through LoadLibrary/GetProcAddress (Framework4.0 + x86/x64 only; PS2EXE has no equivalent). Fixed two-export script (Add, Greet):
| Build | Output size | Warm compile |
|---|---|---|
| ps12exe · DLL export · Framework4.0 | 16384 bytes | ~1864 ms |
| PS2EXE 1.0.18 · DLL export | not support | not support |
Whether native child processes started by the EXE see a real console TTY (#59), whether the script can read raw stdin (#62), and whether the special path variables resolve — all verified from a real console window on Windows 11:
| Capability | ps12exe | MScholtes/PS2EXE@1.0.18 |
|---|---|---|
Native child process sees a console TTY (isTTY) |
✔️ | ❌ |
Raw stdin ([Console]::In) readable |
✔️ (unless the script uses $input) |
❌ |
$PSCommandPath / $PSScriptRoot resolve |
✔️ (exe path / exe directory) | ❌ |
| Command-line arguments parsed as PSD data (tables/objects) | ✔️ (-Config "@{...}") |
❌ (strings only) |
PS2EXE 1.0.18 always pipes script output through Out-String and eagerly drains redirected stdin before the script runs, so native children lose the console handle and stdin reaches EOF; ps12exe runs the script through the host (Out-Default) and only drains stdin when the script actually uses $input. PS2EXE also leaves $PSCommandPath/$PSScriptRoot empty inside the compiled program (it offers its own $ScriptRoot instead), while ps12exe maps both to the generated exe.
Compared to MScholtes/PS2EXE@1.0.18, this project offers the following improvements:
| Improvement | Description |
|---|---|
| ✔️ Syntax check during compilation | Syntax checking at compile time for better code quality |
| ⚡ Compile-time constant evaluation | Side-effect-free scripts are evaluated at build time and emitted as ~1 KB exes |
| 🧬 PowerShell Core / cross-platform target | Build.Target Core targets PowerShell 7+ on Windows, Linux and macOS |
| 🔄 Powerful preprocessing | Preprocess the script before compilation, no need to copy and paste everything into the script |
🛠️ Build.Options parameter |
New parameter to let you further customize the generated executable |
📦️ Build.Minify parameter |
Preprocess the script before compilation to generate a smaller executable |
| 🌐 Support for compiling scripts and included files from URLs | Support for downloading icons from URLs |
🖥️ Optimization of App.Windowed parameter |
Optimized option handling and window title display; you can now set the title of the custom pop-up |
| ✍️ Code signing and icon auto-conversion | Sign output with a PFX certificate or a store thumbprint, and convert icons automatically |
🧰 Extras: exe21sp, web server, context menu, interact mode |
Decompile exes, compile online, right-click compile and more |
| 🧹 Removed exe files | Removed exe files from the code repo |
| 🌍 Multilingual support, pure script GUI | Better multilingual support, pure script GUI, supports dark mode |
| 📖 Separated cs files from ps1 files | Easier to read and maintain |
| 🚀 More improvements | And more |







