ps12exe.ps1
#Requires -Version 5.0 <# .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 ps12exeGUI for convenience. 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 file path or url to convert to executable (file has to be UTF8 or UTF16 encoded) .PARAMETER Content The content of the PowerShell script to convert to executable .PARAMETER outputFile destination executable file name or folder, defaults to inputFile with extension '.exe' .PARAMETER CompilerOptions additional compiler options (see https://msdn.microsoft.com/en-us/library/78f4aasd.aspx) .PARAMETER TempDir directory for storing temporary files (default is random generated temp directory in %temp%) .PARAMETER minifyer scriptblock to minify the script before compiling .PARAMETER lcid location ID for the compiled executable. Current user culture if not specified .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 prepareDebug create helpful information for debugging of generated executable. See parameter -debug there .PARAMETER architecture compile for specific runtime only. Possible values are 'x64' and 'x86' and 'anycpu' .PARAMETER threadingModel Threading model for the compiled executable. Possible values are 'STA' and 'MTA' .PARAMETER resourceParams A hashtable that contains resource parameters for the compiled executable. Possible keys are 'iconFile', 'title', 'description', 'company', 'product', 'copyright', 'trademark', 'version' iconFile can be a file path or url to an icon file. All other values are strings. see https://msdn.microsoft.com/en-us/library/system.reflection.assemblytitleattribute(v=vs.110).aspx for details .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 configFile write a config file (<outputfile>.exe.config) .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) .PARAMETER targetRuntime the target runtime to compile for. Possible values are 'Framework4.0' or 'Framework2.0', default is 'Framework4.0' .PARAMETER GuestMode Compile scripts with additional protection, prevent native files from being accessed .EXAMPLE ps12exe C:\Data\MyScript.ps1 Compiles C:\Data\MyScript.ps1 to C:\Data\MyScript.exe as console executable .EXAMPLE ps12exe -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 #> [CmdletBinding(DefaultParameterSetName = 'InputFile')] Param( [Parameter(ParameterSetName = 'InputFile', Position = 0)] [ValidatePattern("^(https?|ftp)://.*|.*\.(ps1|psd1|tmp)$")] [String]$inputFile, [Parameter(ParameterSetName = 'Content', ValueFromPipeline = $TRUE)] [String]$Content, [Parameter(ParameterSetName = 'InputFile', Position = 1)] [Parameter(ParameterSetName = 'Content', Position = 0)] [ValidatePattern(".*\.(exe|com|bin)$")] [String]$outputFile = $NULL, [String]$CompilerOptions = '/o+ /debug-', [String]$TempDir = $NULL, [scriptblock]$minifyer = $null, [Switch]$noConsole, [Switch]$prepareDebug, [int]$lcid, [ValidateSet('x64', 'x86', 'anycpu')] [String]$architecture = 'anycpu', [ValidateSet('STA', 'MTA')] [String]$threadingModel = 'STA', [HashTable]$resourceParams = @{}, [Switch]$UNICODEEncoding, [Switch]$credentialGUI, [Switch]$configFile, [Switch]$noOutput, [Switch]$noError, [Switch]$noVisualStyles, [Switch]$exitOnCancel, [Switch]$DPIAware, [Switch]$winFormsDPIAware, [Switch]$requireAdmin, [Switch]$supportOS, [Switch]$virtualize, [Switch]$longPaths, [ValidateSet('Framework2.0', 'Framework4.0')] [String]$targetRuntime = 'Framework4.0', #_if PSScript [ArgumentCompleter({ Param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) . "$PSScriptRoot\src\LocaleArgCompleter.ps1" @PSBoundParameters })] #_endif [Switch]$GuestMode, [string]$Localize, [Switch]$help, # TODO,不进入文档 [Parameter(DontShow)] [switch]$UseWindowsPowerShell = $true, # 兼容旧版参数列表,不进入文档 [Parameter(DontShow)] [Switch]$noConfigFile, [Parameter(DontShow)] [Switch]$x86, [Parameter(DontShow)] [Switch]$x64, [Parameter(DontShow)] [Switch]$STA, [Parameter(DontShow)] [Switch]$MTA, [Parameter(DontShow)] [String]$iconFile, [Parameter(DontShow)] [String]$title, [Parameter(DontShow)] [String]$description, [Parameter(DontShow)] [String]$company, [Parameter(DontShow)] [String]$product, [Parameter(DontShow)] [String]$copyright, [Parameter(DontShow)] [String]$trademark, [Parameter(DontShow)] [String]$version, [Parameter(DontShow)] [Switch]$runtime20, [Parameter(DontShow)] [Switch]$runtime40, # 内部参数,不进入文档 [Parameter(DontShow)] [Switch]$nested, [Parameter(DontShow)] [string]$DllExportList ) $Verbose = $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent $Debug = $DebugPreference -ne 'SilentlyContinue' function RollUp { param ($num = 1, [switch]$InVerbose) if (-not ($Verbose -or $InVerbose -or $Debug)) { if ($Host.UI.SupportsVirtualTerminal) { Write-Host $([char]27 + '[' + $num + 'A') -NoNewline } elseif (-not $nested) { $CousorPos = $Host.UI.RawUI.CursorPosition try { $CousorPos.Y = $CousorPos.Y - $num $Host.UI.RawUI.CursorPosition = $CousorPos } catch { $Error.RemoveAt(0) } } } } if ($Debug) { $DebugPreference = 'Continue' } # fix -debug sets it to 'Inquire' #_if PSScript $LocaleLoaderArg = @{ Localize = $Localize } if ($nested) { $LocaleLoaderArg.FaildLoadLocaleData = {} } #_endif $LocalizeData = #_if PSScript . $PSScriptRoot\src\LocaleLoader.ps1 @LocaleLoaderArg #_else #_include "$PSScriptRoot/src/locale/en-UK.ps1" #_endif function Show-Help { . $PSScriptRoot\src\HelpShower.ps1 -HelpData $LocalizeData.ConsoleHelpData | Write-Host } function Write-I18n( [ValidateSet('Info', 'Warning', 'Error', 'Debug', 'Verbose', 'Host')] $PipeLineType, $Mid, $FormatArgs, $ErrorId = $Mid, [System.Management.Automation.ErrorCategory]$Category = 'NotSpecified', $TargetObject, $Exception, $ForegroundColor = $Host.UI.RawUI.ForegroundColor ) { $value = $LocalizeData.CompilingI18nData[$Mid] -f $FormatArgs if (!$value) { $value = "fatal error: No i18n data for $Mid" } switch ($PipeLineType) { 'Info' { Write-Information $value } 'Warning' { Write-Warning $value } 'Error' { $Host.UI.RawUI.ForegroundColor = "Red" $Host.UI.WriteErrorLine($value) $Host.UI.RawUI.ForegroundColor = $ForegroundColor if (!$Exception) { $Exception = New-Object System.Exception } Write-Error -Exception $Exception -Message $value -Category $Category -ErrorId $ErrorId -TargetObject $TargetObject -ErrorAction SilentlyContinue } 'Debug' { Write-Debug $value } 'Host' { Write-Host $value -ForegroundColor $ForegroundColor } 'Verbose' { Write-Verbose $value } } } if ($help) { Show-Help return } if (-not ($inputFile -or $Content)) { Show-Help Write-Host Write-I18n Error NoneInput -Category InvalidArgument return } $Params = $PSBoundParameters $ParamList = $MyInvocation.MyCommand.Parameters $Params.Remove('Content') | Out-Null #防止回滚覆盖 $Params.Remove('DllExportList') | Out-Null function bytesOfString([string]$str) { if ($str) { [system.Text.Encoding]::UTF8.GetBytes($str).Count } else { 0 } } #_if PSScript #在PSEXE中主机永远是winpwsh,所以不会内嵌 if (!$nested) { #_endif [System.Collections.ArrayList]$DllExportList = @() if ($inputFile -and $Content) { Write-I18n Error BothInputAndContentSpecified -Category InvalidArgument return } . $PSScriptRoot\src\ReadScriptFile.ps1 try { if ($inputFile) { $Content = ReadScriptFile $inputFile if (!$Content) { return } if ((bytesOfString $Content) -ne (Get-Item $inputFile -ErrorAction Ignore).Length) { Write-I18n Host PreprocessedScriptSize $(bytesOfString $Content) } } else { $NewContent = Preprocessor ($Content -split '\r?\n') "$PWD\a.ps1" Write-I18n Verbose PreprocessDone if ((bytesOfString $NewContent) -ne (bytesOfString $Content)) { Write-I18n Host PreprocessedScriptSize $(bytesOfString $NewContent) } $Content = $NewContent } } catch { return } if ($minifyer) { Write-I18n Host MinifyingScript try { # get caller's stackframe $Stack = Get-PSCallStack $Frame = $Stack[1] $Variables = $Frame.GetFrameVariables() $Variables._ = [System.Management.Automation.PSVariable]::New('_', $Content) $MinifyedContent = $minifyer.InvokeWithContext(@{}, $Variables.Values, $Variables.args.Value) RollUp Write-I18n Host MinifyedScriptSize $(bytesOfString $MinifyedContent) } catch { Write-I18n Error MinifyerError $_ -Exception $_.Exception } if (-not $MinifyedContent -and $Content) { Write-I18n Warning MinifyerFailedUsingOriginalScript } else { $Content = $MinifyedContent } } #_if PSScript } else { $Content = Get-Content -Raw -LiteralPath $inputFile -Encoding UTF8 -ErrorAction SilentlyContinue if (!$Content) { Write-I18n Error TempFileMissing $inputFile -Category ResourceUnavailable return } if (!$TempDir) { Remove-Item $inputFile -ErrorAction SilentlyContinue } if ($DllExportList) { [System.Collections.ArrayList]$DllExportList = $DllExportList | ConvertFrom-Json } } #_endif # pragma预处理命令可能会修改参数,所以现在开始参数更新 $Params.GetEnumerator() | ForEach-Object { Set-Variable -Name $_.Key -Value $_.Value } # 处理兼容旧版参数列表 if ($x86 -and $x64) { Write-I18n Error CombinedArg_x86_x64 -Category InvalidArgument return } if ($x86) { $architecture = 'x86' } if ($x64) { $architecture = 'x64' } $Params.architecture = $architecture [void]$Params.Remove("x86"); [void]$Params.Remove("x64") if ($runtime20) { foreach ($_ in @("runtime40", "longPaths", "winFormsDPIAware")) { if ($Params[$_]) { Write-I18n Error "CombinedArg_Runtime20_$_" -Category InvalidArgument return } } } if ($runtime20) { $targetRuntime = 'Framework2.0' } if ($runtime40) { $targetRuntime = 'Framework4.0' } $Params.targetRuntime = $targetRuntime [void]$Params.Remove("runtime20"); [void]$Params.Remove("runtime40") if ($STA -and $MTA) { Write-I18n Error CombinedArg_STA_MTA -Category InvalidArgument return } if ($STA) { $threadingModel = 'STA' } if ($MTA) { $threadingModel = 'MTA' } $Params.threadingModel = $threadingModel [void]$Params.Remove("STA"); [void]$Params.Remove("MTA") $resourceParamKeys = @('iconFile', 'title', 'description', 'company', 'product', 'copyright', 'trademark', 'version') $resourceParamKeys | ForEach-Object { if ($Params.ContainsKey($_)) { $resourceParams[$_] = $Params[$_] } [void]$Params.Remove($_) } $resourceParams.GetEnumerator() | ForEach-Object { if (-not $resourceParamKeys.Contains($_.Key)) { Write-I18n Warning InvalidResourceParam $_.Key } } if ($configFile -and $noConfigFile) { Write-I18n Error CombinedArg_ConfigFileYes_No -Category InvalidArgument return } if ($noConfigFile) { $configFile = $FALSE } $NoResource = -not $resourceParams.Count # 由于其他的resourceParams参数需要转义,iconFile参数不需要转义,所以提取出来单独处理 $iconFile = $resourceParams['iconFile'] $resourceParams.Remove('iconFile') # 语法检查 $SyntaxErrors = if ($targetRuntime -eq 'Framework2.0') { #_if PSScript powershell -version 2.0 -OutputFormat xml -file $PSScriptRoot/src/RuntimePwsh2.0/CodeChecker.ps1 -scriptText $Content #_else #_include_as_value Pwsh2CodeCheckerCodeStr $PSScriptRoot/src/RuntimePwsh2.0/CodeChecker.ps1 #_!! powershell -version 2.0 -OutputFormat xml -Command "&{$Pwsh2CodeCheckerCodeStr} -scriptText '$($Content -replace "'","''")'" #_endif } if (!$SyntaxErrors) { $Tokens = $null $AST = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$SyntaxErrors) } if ($SyntaxErrors) { Write-I18n Error -Category ParserError -TargetObject $SyntaxErrors InputSyntaxError return } # retrieve absolute paths independent if path is given relative oder absolute if (-not $inputFile) { $inputFile = '.\a.ps1' } if ($inputFile -notmatch "^(https?|ftp)://") { $inputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($inputFile) } if (-not $outputFile) { if ($inputFile -match "^https?://") { $outputFile = ([System.IO.Path]::Combine($PWD, [System.IO.Path]::GetFileNameWithoutExtension($inputFile) + ".exe")) } else { $outputFile = ([System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($inputFile), [System.IO.Path]::GetFileNameWithoutExtension($inputFile) + ".exe")) } } else { $outputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($outputFile) if ((Test-Path $outputFile -PathType Container)) { $outputFile = ([System.IO.Path]::Combine($outputFile, [System.IO.Path]::GetFileNameWithoutExtension($inputFile) + ".exe")) } } #_if PSScript #在PSEXE中主机永远是winpwsh,可省略该部分 . $PSScriptRoot/src/PSObjectToString.ps1 function UsingWinPowershell($Boundparameters) { # starting Windows Powershell $Params = ([hashtable]$Boundparameters).Clone() $Params.Remove("minifyer") $Params.Remove("Content") $Params.Remove("inputFile") $Params.Remove("outputFile") $Params.Remove("resourceParams") #使用旧版参数列表传递hashtable参数更为保险 $TempFile = if ($TempDir) { [System.IO.Path]::Combine($TempDir, 'main.ps1') } else { [System.IO.Path]::GetTempFileName() } $Content | Set-Content $TempFile -Encoding UTF8 -NoNewline $Params.Add("outputFile", $outputFile) $Params.Add("inputFile", $TempFile) if ($TempDir) { $Params.TempDir = $TempDir } $resourceParamKeys | ForEach-Object { if ($resourceParams.ContainsKey($_) -and $resourceParams[$_]) { $Params[$_] = $resourceParams[$_] } } if ($iconFile) { $Params.iconFile = $iconFile } if ($DllExportList.Length) { $Params.DllExportList = ConvertTo-Json -depth 7 -Compress -InputObject $DllExportList } $CallParam = Get-ArgsString $Params Write-Debug "Starting WinPowershell ps12exe with parameters: $CallParam" powershell -noprofile -Command "&'$PSScriptRoot\ps12exe.ps1' $CallParam -nested" | Write-Host return } if (!$nested -and ($PSVersionTable.PSEdition -eq "Core") -and $UseWindowsPowerShell -and (Get-Command powershell -ErrorAction Ignore)) { UsingWinPowershell $Params return } #_endif if ($inputFile -eq $outputFile) { Write-I18n Error IdenticalInputOutput -Category InvalidArgument return } if ($winFormsDPIAware) { $supportOS = $TRUE } if ($virtualize) { foreach ($_ in @("requireAdmin", "supportOS", "longPaths")) { if ($Params[$_]) { Write-I18n Error "CombinedArg_Virtualize_$_" -Category InvalidArgument return } } } if (!$configFile) { foreach ($_ in @("longPaths", "winFormsDPIAware")) { if ($Params[$_]) { Write-I18n Warning "CombinedArg_NoConfigFile_$_" -Category InvalidArgument $configFile = $true } } } # escape escape sequences in version info $resourceParamKeys | ForEach-Object { if ($resourceParams.ContainsKey($_)) { $resourceParams[$_] = $resourceParams[$_] -replace "\\", "\\" } } . $PSScriptRoot\src\AstAnalyze.ps1 $AstAnalyzeResult = AstAnalyze $Ast Write-Debug "AstAnalyzeResult: $(($AstAnalyzeResult|ConvertTo-Json) -split "\r?\n" -ne '' -join "`n")" $CommandNames = (Get-Command).Name + (Get-Alias).Name $FindedCmdlets = @() $NotFindedCmdlets = @() $AstAnalyzeResult.UsedNonConstFunctions | ForEach-Object { if ($_ -match '\$' -or -not $_) { return } if ($CommandNames -notcontains $_) { if ($_ -match '^[\w\-_]+$' -and (Get-Command $_ -ErrorAction Ignore)) { $FindedCmdlets += $_ } # 跳过成员函数,因为解析Add-Type太过复杂 elseif (-not $_.Contains(']::')) { $NotFindedCmdlets += $_ } } } if ($FindedCmdlets) { Write-I18n Warning SomeCmdletsMayNotAvailable $($FindedCmdlets -join '、') } if ($NotFindedCmdlets) { Write-I18n Warning SomeNotFindedCmdlets $($NotFindedCmdlets -join '、') } try { . $PSScriptRoot\src\InitCompileThings.ps1 #_if PSScript if (-not $noConsole -and $AstAnalyzeResult.IsConst -and -not $iconFile -and -not $requireAdmin) { # TODO: GUI(MassageBoxW)、icon Write-I18n Verbose TryingTinySharpCompile Write-I18n Host CompilingFile try { . $PSScriptRoot\src\TinySharpCompiler.ps1 $TinySharpSuccess = $TRUE } catch { RollUp Write-I18n Verbose TinySharpFailedFallback Write-Error $_ } } #_endif try { if (!$TinySharpSuccess) { Write-I18n Host CompilingFile if ($targetRuntime -eq 'Framework2.0') { $TargetFramework = ".NETFramework,Version=v2.0" } if ($PSVersionTable.PSEdition -eq "Core") { # unfinished! if (!$TargetFramework) { $Info = [System.Environment]::Version $TargetFramework = ".NETCore,Version=v$($Info.Major).$($Info.Minor)" } . $PSScriptRoot\src\CodeAnalysisCompiler.ps1 } else { if (!$TargetFramework) { $TargetFramework = ".NETFramework,Version=v4.7" } . $PSScriptRoot\src\CodeDomCompiler.ps1 } } RollUp } catch { RollUp Write-I18n Host CompilationFailed -ForegroundColor Red throw $_ } if (!(Test-Path $outputFile)) { Write-I18n Error OutputFileNotWritten -Category WriteError return } else { #_if PSScript if (-not $TinySharpSuccess) { & $PSScriptRoot\src\ExeSinker.ps1 $outputFile -removeResources:$( $NoResource -and $AstAnalyzeResult.IsConst -and -not $requireAdmin ) -removeVersionInfo:$($resourceParams.Count -eq 0) } #_endif Write-I18n Host CompiledFileSize $((Get-Item $outputFile).Length) Write-I18n Verbose OutputPath $outputFile if ($configFile) { $configFileForEXE3 | Set-Content ($outputFile + ".config") -Encoding UTF8 Write-I18n Host ConfigFileCreated } if ($prepareDebug) { $cr.TempFiles | Where-Object { $_ -ilike "*.cs" } | Select-Object -First 1 | ForEach-Object { $dstSrc = ([System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($outputFile), [System.IO.Path]::GetFileNameWithoutExtension($outputFile) + ".cs")) Write-I18n Host SourceFileCopied $dstSrc Copy-Item -Path $_ -Destination $dstSrc -Force } $cr.TempFiles | Remove-Item -Verbose:$FALSE -Force -ErrorAction SilentlyContinue } } } catch { if (Test-Path $outputFile) { Remove-Item $outputFile -Verbose:$FALSE } $_ | Write-Error -ErrorAction Continue if ($PSVersionTable.PSEdition -eq "Core" -and (Get-Command powershell -ErrorAction Ignore)) { Write-I18n Host RoslynFailedFallback -ForegroundColor Yellow UsingWinPowershell $Params } elseif (!$GuestMode) { $githubfeedback = "https://github.com/steve02081504/ps12exe/issues/new?assignees=steve02081504&labels=bug&projects=&template=bug-report.yaml" $urlParams = @{ title = "$_" "latest-release" = if (Get-Module -ListAvailable ps12exe) { "true" } else { "false" } "bug-description" = 'Compilation failed' "expected-behavior" = 'Compilation should succeed' "additional-context" = @" Version infos: `````` $($PSVersionTable | Format-List | Out-String) `````` Error message: `````` $($_ | Format-List | Out-String) `````` "@ } foreach ($key in $urlParams.Keys) { $githubfeedback += "&$key=$([system.uri]::EscapeDataString($urlParams[$key]))" } Write-I18n Host OppsSomethingWentWrong -ForegroundColor Yellow $versionNow = Get-Module -ListAvailable ps12exe | Sort-Object -Property Version -Descending | Select-Object -First 1 $versionOnline = Find-Module ps12exe | Sort-Object -Property Version -Descending | Select-Object -First 1 if ("$($versionNow.Version)" -ne "$($versionOnline.Version)") { Write-I18n Host TryUpgrade $($versionOnline.Version) -ForegroundColor Yellow } else { Write-I18n Host EnterToSubmitIssue -ForegroundColor Yellow Read-Host | Out-Null Start-Process $githubfeedback } } } finally { if ($TempTempDir) { Remove-Item $TempTempDir -Recurse -Force -ErrorAction SilentlyContinue } } |