PS2EXE2ps12exe.psm1
|
<# .SYNOPSIS PS2EXE2ps12exe is a module to hook all PS2EXE calls into ps12exe. #> <# .SYNOPSIS Converts powershell scripts to standalone executables. .DESCRIPTION Converts powershell scripts to standalone executables. GUI output and input is activated with one switch, real windows executables are generated. You may use the graphical front end Win-PS2EXE for convenience. PS2EXE2ps12exe is a compatibility layer: it forwards and rewrites every PS2EXE call into ps12exe. Features that ps12exe does not provide natively (conHost, embedFiles) are emulated by rewriting the script at compile time, so PS2EXE scripts keep working unchanged. Please see Remarks on project page for topics "GUI mode output formatting", "Config files", "Password security", "Script variables" and "Window in background in -noConsole mode". .PARAMETER inputFile Powershell script to convert to executable (file has to be UTF8 or UTF16 encoded) .PARAMETER outputFile destination executable file name or folder, defaults to inputFile with extension '.exe' .PARAMETER prepareDebug create helpful information for debugging of generated executable. See parameter -debug there .PARAMETER runtime20 legacy: this switch forces the generated executable to target .NET Framework 2.0/3.x for PowerShell 2.0 .PARAMETER runtime40 legacy: this switch forces the generated executable to target .NET Framework 4.x for PowerShell 3.0 or higher .PARAMETER x86 compile for 32-bit runtime only .PARAMETER x64 compile for 64-bit runtime only .PARAMETER lcid location ID for the compiled executable. Current user culture if not specified .PARAMETER STA Single Thread Apartment mode .PARAMETER MTA Multi Thread Apartment mode .PARAMETER nested internal use .PARAMETER noConsole the resulting executable will be a Windows Forms app without a console window. You might want to pipe your output to Out-String to prevent a message box for every line of output (example: dir C:\ | Out-String) .PARAMETER conHost force start with conhost as console instead of Windows Terminal. If necessary a new console window will appear. Mapped to ps12exe's ``-App @{ConHost=`$true}`` (native conhost support). .PARAMETER core compile the script with the .NET SDK to a PowerShell Core executable (ps12exe ``-Build @{Target='Core'}``). See the PS2EXE.Core parameters below; they are forwarded to ps12exe's ``-Build @{Core=@{...}}``. .PARAMETER TargetOS target operating system for a Core executable ('Windows', 'Linux' or 'MacOS'). .PARAMETER TargetFramework target .NET framework moniker for a Core executable (for example 'net8.0'). .PARAMETER PowerShellVersion version of the PowerShell SDK bundled when using the Bundled backend. .PARAMETER SelfContained bundle the .NET runtime into a Core executable (Bundled backend). .PARAMETER PublishSingleFile publish a Core executable as a single file (default true). .PARAMETER Trimmed enable IL trimming for a Core executable (Bundled backend). .PARAMETER TrimMode trimming aggressiveness: 'partial' (default) or 'full'. .PARAMETER ReadyToRun pre-compile assemblies for faster startup (Bundled backend). .PARAMETER InvariantGlobalization use invariant globalization for a Core executable (Bundled backend). .PARAMETER AOT experimental Native AOT compilation; requires -SelfContained (Bundled backend). .PARAMETER Quiet suppress informational output during compilation. .PARAMETER UNICODEEncoding encode output as UNICODE in console mode, useful to display special encoded chars .PARAMETER credentialGUI use GUI for prompting credentials in console mode instead of console input .PARAMETER iconFile icon file name for the compiled executable .PARAMETER embedFiles paths to files to embed given as hash table, will be extracted at runtime to the keys of the hashes, source file names must be unique, e.g. -embedFiles @{'Targetfilepath'='Sourcefilepath'}. Absolute and relative paths are allowed. For target paths a relative path beginning with '.\' is interpreted as relative to the executable, without the leading '.\' as relative to the current path at runtime. Directories are created automatically on startup if necessary. In the target path environment variables in cmd.exe notation like %TEMP% or %APPDATA% are expanded at runtime. ps12exe has no native equivalent, so PS2EXE2ps12exe embeds the files at compile time and extracts them at startup. .PARAMETER title title information (displayed in details tab of Windows Explorer's properties dialog) .PARAMETER description description information (not displayed, but embedded in executable) .PARAMETER company company information (not displayed, but embedded in executable) .PARAMETER product product information (displayed in details tab of Windows Explorer's properties dialog) .PARAMETER copyright copyright information (displayed in details tab of Windows Explorer's properties dialog) .PARAMETER trademark trademark information (displayed in details tab of Windows Explorer's properties dialog) .PARAMETER version version information (displayed in details tab of Windows Explorer's properties dialog) .PARAMETER configFile write a config file (<outputfile>.exe.config) .PARAMETER noConfigFile compatibility parameter .PARAMETER noOutput the resulting executable will generate no standard output (includes verbose and information channel) .PARAMETER noError the resulting executable will generate no error output (includes warning and debug channel) .PARAMETER noVisualStyles disable visual styles for a generated windows GUI application. Only applicable with parameter -noConsole .PARAMETER exitOnCancel exits program when Cancel or "X" is selected in a Read-Host input box. Only applicable with parameter -noConsole .PARAMETER DPIAware if display scaling is activated, GUI controls will be scaled if possible. .PARAMETER winFormsDPIAware creates an entry in the config file for WinForms to use DPI scaling. Forces -configFile and -supportOS .PARAMETER requireAdmin if UAC is enabled, compiled executable will run only in elevated context (UAC dialog appears if required) .PARAMETER supportOS use functions of newest Windows versions (execute [Environment]::OSVersion to see the difference) .PARAMETER virtualize application virtualization is activated (forcing x86 runtime) .PARAMETER longPaths enable long paths ( > 260 characters) if enabled on OS (works only with Windows 10 or up) .EXAMPLE Invoke-ps2exe C:\Data\MyScript.ps1 Compiles C:\Data\MyScript.ps1 to C:\Data\MyScript.exe as console executable .EXAMPLE ps2exe -inputFile C:\Data\MyScript.ps1 -outputFile C:\Data\MyScriptGUI.exe -iconFile C:\Data\Icon.ico -noConsole -title "MyScript" -version 0.0.0.1 Compiles C:\Data\MyScript.ps1 to C:\Data\MyScriptGUI.exe as graphical executable, icon and meta data .EXAMPLE Win-PS2EXE Start graphical front end to Invoke-ps2exe #> function Invoke-ps2exe { [CmdletBinding()] Param([STRING]$inputFile = $NULL, [STRING]$outputFile = $NULL, [SWITCH]$prepareDebug, [SWITCH]$runtime20, [SWITCH]$runtime40, [SWITCH]$x86, [SWITCH]$x64, [SWITCH]$ARM, [int]$lcid, [SWITCH]$STA, [SWITCH]$MTA, [SWITCH]$nested, [SWITCH]$noConsole, [SWITCH]$conHost, [SWITCH]$UNICODEEncoding, [SWITCH]$credentialGUI, [STRING]$iconFile = $NULL, [Hashtable]$embedFiles = @{}, [STRING]$title, [STRING]$description, [STRING]$company, [STRING]$product, [STRING]$copyright, [STRING]$trademark, [STRING]$version, [SWITCH]$configFile, [SWITCH]$noConfigFile, [SWITCH]$noOutput, [SWITCH]$noError, [SWITCH]$noVisualStyles, [SWITCH]$exitOnCancel, [SWITCH]$DPIAware, [SWITCH]$winFormsDPIAware, [SWITCH]$requireAdmin, [SWITCH]$supportOS, [SWITCH]$virtualize, [SWITCH]$longPaths, # PS2EXE.Core 新增的 Core 目标参数 [SWITCH]$Core, [ValidateSet('Windows', 'Linux', 'MacOS')][STRING]$TargetOS, [ValidateSet('net6.0', 'net7.0', 'net8.0', 'net9.0', 'net10.0', 'net11.0')][STRING]$TargetFramework, [STRING]$PowerShellVersion, [SWITCH]$SelfContained, [SWITCH]$PublishSingleFile, [SWITCH]$Trimmed, [ValidateSet('partial', 'full')][STRING]$TrimMode, [SWITCH]$ReadyToRun, [SWITCH]$InvariantGlobalization, [SWITCH]$AOT, [SWITCH]$Quiet) # 复刻 PS2EXE 的参数校验 if ($x86 -and $x64) { throw "-x86 can't be combined with -x64." } if ($STA -and $MTA) { throw "-STA can't be combined with -MTA." } if ($noConsole -and $conHost) { throw "-noConsole cannot be combined with -conHost" } if ($configFile -and $noConfigFile) { throw "-configFile cannot be combined with -noConfigFile" } if ($runtime20 -and $runtime40) { throw "-runtime20 can't be combined with -runtime40." } if ($runtime20 -and $longPaths) { throw "Long paths are only available with .NET 4 or above." } if ($runtime20 -and $winFormsDPIAware) { throw "DPI awareness is only available with .NET 4 or above." } # PS2EXE.Core 的 Core 校验 if ($x86 -and $ARM) { throw "-x86 can't be combined with -ARM." } if ($x64 -and $ARM) { throw "-x64 can't be combined with -ARM." } if ($Core -and ($runtime20 -or $runtime40)) { throw "-Core can't be combined with -runtime20/-runtime40." } $coreOnlyParams = @('TargetOS', 'TargetFramework', 'PowerShellVersion', 'SelfContained', 'PublishSingleFile', 'Trimmed', 'TrimMode', 'ReadyToRun', 'InvariantGlobalization', 'AOT') $coreOnlySet = @($coreOnlyParams | Where-Object { $PSBoundParameters.ContainsKey($_) }) if ($ARM) { $coreOnlySet = @('ARM') + $coreOnlySet } if (-not $Core -and $coreOnlySet.Count) { throw "-Core is required for: $($coreOnlySet -join ', ')." } if ($AOT -and -not $SelfContained) { throw "-AOT requires -SelfContained." } if ($PSBoundParameters.ContainsKey('TrimMode') -and -not $Trimmed) { throw "-TrimMode requires -Trimmed." } if (!(Get-Module -Name ps12exe -ListAvailable)) { Install-Module -Name ps12exe -Scope CurrentUser -Force } Import-Module -Name ps12exe # 转发到 ps12exe 的对象 API $psParams = @{} foreach ($name in @('inputFile', 'outputFile', 'configFile')) { if ($PSBoundParameters.ContainsKey($name)) { $psParams[$name] = $PSBoundParameters[$name] } } # App $app = @{} if ($PSBoundParameters.ContainsKey('noConsole')) { $app.Windowed = [bool]$noConsole } if ($PSBoundParameters.ContainsKey('UNICODEEncoding')) { $app.OutputEncoding = if ($UNICODEEncoding) { 'UTF16LE' } else { 'Default' } } if ($PSBoundParameters.ContainsKey('credentialGUI')) { $app.CredentialGUI = [bool]$credentialGUI } if ($PSBoundParameters.ContainsKey('noVisualStyles')) { $app.VisualStyles = -not [bool]$noVisualStyles } if ($PSBoundParameters.ContainsKey('exitOnCancel')) { $app.ExitOnCancel = [bool]$exitOnCancel } if ($PSBoundParameters.ContainsKey('DPIAware')) { $app.DpiAware = [bool]$DPIAware } if ($PSBoundParameters.ContainsKey('winFormsDPIAware')) { $app.WinFormsDpiAware = [bool]$winFormsDPIAware } if ($PSBoundParameters.ContainsKey('conHost')) { $app.ConHost = [bool]$conHost } $silence = @() if ($noOutput) { $silence += @('Output', 'Verbose') } if ($noError) { $silence += @('Error', 'Warning', 'Debug') } if ($silence.Count) { $app.Silence = $silence } if ($app.Count) { $psParams.App = $app } # Os $os = @{} if ($PSBoundParameters.ContainsKey('requireAdmin')) { $os.Admin = [bool]$requireAdmin } if ($PSBoundParameters.ContainsKey('supportOS')) { $os.ModernOS = [bool]$supportOS } if ($PSBoundParameters.ContainsKey('virtualize')) { $os.Virtualize = [bool]$virtualize } if ($PSBoundParameters.ContainsKey('longPaths')) { $os.LongPaths = [bool]$longPaths } if ($os.Count) { $psParams.Os = $os } # Build $build = @{} if ($PSBoundParameters.ContainsKey('prepareDebug')) { $build.KeepSource = [bool]$prepareDebug } if ($PSBoundParameters.ContainsKey('lcid')) { $build.Culture = "$lcid" } if ($x86) { $build.Platform = 'x86' } elseif ($x64) { $build.Platform = 'x64' } elseif ($ARM) { $build.Platform = 'arm64' } if ($STA) { $build.Apartment = 'STA' } elseif ($MTA) { $build.Apartment = 'MTA' } if ($runtime20) { $build.Target = 'Framework2.0' } elseif ($runtime40) { $build.Target = 'Framework4.0' } if ($Core) { $build.Target = 'Core' } # PS2EXE.Core 的 Core 专属参数 → Build.Core 嵌套对象(仅显式给出时写入) $coreOpt = @{} if ($PSBoundParameters.ContainsKey('TargetOS')) { $coreOpt.TargetOs = $TargetOS } if ($PSBoundParameters.ContainsKey('TargetFramework')) { $coreOpt.TargetFramework = $TargetFramework } if ($PSBoundParameters.ContainsKey('PowerShellVersion')) { $coreOpt.PowerShellVersion = "$PowerShellVersion" } if ($PSBoundParameters.ContainsKey('SelfContained')) { $coreOpt.SelfContained = [bool]$SelfContained } if ($PSBoundParameters.ContainsKey('PublishSingleFile')) { $coreOpt.SingleFile = [bool]$PublishSingleFile } if ($PSBoundParameters.ContainsKey('Trimmed')) { $coreOpt.Trimmed = [bool]$Trimmed } if ($PSBoundParameters.ContainsKey('TrimMode')) { $coreOpt.TrimMode = $TrimMode } if ($PSBoundParameters.ContainsKey('ReadyToRun')) { $coreOpt.ReadyToRun = [bool]$ReadyToRun } if ($PSBoundParameters.ContainsKey('InvariantGlobalization')) { $coreOpt.InvariantGlobalization = [bool]$InvariantGlobalization } if ($PSBoundParameters.ContainsKey('AOT')) { $coreOpt.Aot = [bool]$AOT } if ($coreOpt.Count) { $build.Core = $coreOpt } if ($PSBoundParameters.ContainsKey('Quiet')) { $psParams.Quiet = [bool]$Quiet } # noConfigFile 是 PS2EXE 的兼容占位参数,直接忽略 # 资源参数合并为 Resources 哈希表 $resourceKeyMap = @{ iconFile = 'Icon'; title = 'Title'; description = 'Description'; company = 'Company' product = 'Product'; copyright = 'Copyright'; trademark = 'Trademark'; version = 'Version' } $resources = @{} foreach ($name in @('iconFile', 'title', 'description', 'company', 'product', 'copyright', 'trademark', 'version')) { if ($PSBoundParameters.ContainsKey($name) -and $PSBoundParameters[$name]) { $resources[$resourceKeyMap[$name]] = $PSBoundParameters[$name] } } if ($resources.Count) { $psParams.Resources = $resources } # 收集 embedFiles 的二进制内容,供编译期注入 $embedEntries = @() if ($embedFiles) { foreach ($target in $embedFiles.Keys) { $source = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath([string]$embedFiles[$target]) if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { throw "Embed file not found: $($embedFiles[$target])" } $embedEntries += @{ Target = [string]$target Base64 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($source)) } } } # ps12exe 缺少 embedFiles,且不提供 PS2EXE 的 $ScriptRoot;用 minifyer 在预处理后注入。 # 需要转写时总会走这里,未命中任何注入点时原样返回。(conHost 已由 ps12exe 原生 App.ConHost 支持。) $rewriteState = @{ Embeds = $embedEntries } $build.Minify = { $text = $_ if (-not $text) { return $text } $ast = $null try { $ast = [System.Management.Automation.Language.Parser]::ParseInput($text, [ref]$null, [ref]$null) } catch { $ast = $null } if (-not $ast) { return $text } $inject = [System.Collections.Generic.List[string]]::new() foreach ($embed in $rewriteState.Embeds) { $targetLiteral = $embed.Target.Replace("'", "''") $inject.Add(@" & { `$__PSEXE_Target = [System.Environment]::ExpandEnvironmentVariables('$targetLiteral') if (`$__PSEXE_Target.StartsWith('.\')) { `$__PSEXE_Target = [System.IO.Path]::Combine(`$PSScriptRoot, `$__PSEXE_Target.Substring(2)) } `$__PSEXE_Dir = [System.IO.Path]::GetDirectoryName(`$__PSEXE_Target) if (`$__PSEXE_Dir) { [System.IO.Directory]::CreateDirectory(`$__PSEXE_Dir) | Out-Null } [System.IO.File]::WriteAllBytes(`$__PSEXE_Target, [System.Convert]::FromBase64String('$($embed.Base64)')) } "@) } # PS2EXE 会在运行空间里设置 $ScriptRoot;脚本用到时才补上 $scriptRootUsed = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.VariableExpressionAst] -and $node.VariablePath.UserPath -eq 'ScriptRoot' }, $true) if ($scriptRootUsed.Count -gt 0) { $inject.Add('$global:ScriptRoot = $PSScriptRoot') } if ($inject.Count -eq 0) { return $text } $insertAt = if ($ast.ParamBlock) { $ast.ParamBlock.Extent.EndOffset } else { 0 } $text.Substring(0, $insertAt) + "`n" + ($inject -join "`n") + "`n" + $text.Substring($insertAt) }.GetNewClosure() $psParams.Build = $build ps12exe @psParams } function Invoke-WinPS2EXE { if (!(Get-Module -Name ps12exe -ListAvailable)) { Install-Module -Name ps12exe -Scope CurrentUser -Force } Import-Module -Name ps12exe ps12exeGUI } Set-Alias ps2exe Invoke-ps2exe -Scope Global Set-Alias ps2exe.ps1 Invoke-ps2exe -Scope Global Set-Alias Win-PS2EXE Invoke-WinPS2EXE -Scope Global Set-Alias Win-PS2EXE.exe Invoke-WinPS2EXE -Scope Global Export-ModuleMember -Function @('Invoke-PS2EXE', 'Invoke-WinPS2EXE') Export-ModuleMember -Alias @('ps2exe', 'ps2exe.ps1', 'Win-PS2EXE', 'Win-PS2EXE.exe') |