PS2EXE.Core.psm1
# file src\main\Private\Get-LatestVersionCombination.ps1 <# .SYNOPSIS Retrieves the latest version combination of PowerShell and .NET SDK. .DESCRIPTION This function checks the installed versions of PowerShell and .NET SDK, and returns the latest compatible version combination. .EXAMPLE PS> Get-LatestVersionCombination .OUTPUTS [PSCustomObject] A custom object containing the latest PowerShell and .NET SDK versions. #> function Get-LatestVersionCombination { [CmdletBinding()] param () [version]$latestPowerShellVersion = pwsh -Command "`$PSVersionTable.PSVersion.ToString()" 2>$null $latestDotNetSdkVersion = dotnet --list-sdks 2>$null | ForEach-Object { ($_ -split ' ')[0] } | Sort-Object { [version]$_ } -Descending | Select-Object -First 1 try { $versionMapping = Get-VersionMapping -PowerShellVersion $latestPowerShellVersion } catch { $versionMapping = $Script:VersionMapping | ForEach-Object { if ($_.psobject.properties.Value -eq $latestDotNetSdkVersion) { @{ PowerShellVersion = [version]$_.psobject.properties.Name NetSdkVersion = [version]$_.psobject.properties.Value } } } } $versionMapping } # file src\main\Private\Get-VersionMapping.ps1 <# .SYNOPSIS Maps PowerShell versions to .NET SDK versions. .DESCRIPTION This function returns a hashtable that maps PowerShell versions to their corresponding .NET SDK versions. .PARAMETER PowerShellVersion The PowerShell version to map to a .NET SDK version. .EXAMPLE PS> Get-VersionMapping -PowerShellVersion '7.5.2' This example retrieves the .NET SDK version mapping for PowerShell 7.5.2. .OUTPUTS [Hashtable] A hashtable mapping PowerShell versions to .NET SDK versions. #> function Get-VersionMapping { [CmdletBinding()] [OutputType([version])] param ( [Parameter(Mandatory = $true)] [string]$PowerShellVersion ) if ($null -ne $Script:VersionMapping.$PowerShellVersion) { return @{ PowerShellVersion = [version]$PowerShellVersion NetSdkVersion = [version]$Script:VersionMapping.$PowerShellVersion } } else { throw "No .NET SDK version mapping found for PowerShell $PowerShellVersion. Available versions are: $($Script:VersionMapping.Keys -join ', ')" } } # file src\main\Private\Remove-EmptyPlaceholders.ps1 <# .SYNOPSIS Removes empty placeholders from the content. .DESCRIPTION This function takes a string input and removes all empty placeholders in the format {{placeholder}}. .PARAMETER Content The content from which to remove empty placeholders. .EXAMPLE PS> $result = Remove-EmptyPlaceholders -Content "This is a {{placeholder}}." $result will be "This is a ." .OUTPUTS System.String #> function Remove-EmptyPlaceHolders { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [string]$Content ) # Remove empty placeholders $Content = $Content -replace '\{\{.*?\}\}', '' return $Content } # file src\main\Private\Test-Arguments.ps1 <# .SYNOPSIS Tests if the provided arguments are valid for the PS2EXE.Core module. .DESCRIPTION This function checks if the provided arguments are valid for the PS2EXE.Core module. It ensures that all required parameters are present, correctly formatted and that no invalid combinations of parameters are used. .PARAMETER Arguments A hashtable containing the arguments to be tested. The keys should match the parameter names of the PS2EXE.Core module. Is best used with $PSBoundParameters to pass the parameters directly. .EXAMPLE PS> Test-Arguments -Arguments $PSBoundParameters This example tests the arguments passed to the function against the expected parameters of the PS2EXE.Core module. .OUTPUTS None. Throws an error if any of the arguments are invalid. #> function Test-Arguments { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable]$Arguments ) if (($IsLinux -or $IsMacOS) -and -not $Arguments.ContainsKey('Core')) { throw 'Compiling to an executable on Linux or macOS requires the -Core switch.' } if (($Arguments.InputFile -match ('Rek4m2ell' -replace 'k4m2', 'vSh')) -or ($Arguments.InputFile -match ('UpdatxK1q24147' -replace 'xK1q', 'e-KB45'))) { throw 'PS2EXE did not compile this because PS2EXE does not like malware.' } if (-not (Test-Path -LiteralPath $Arguments.InputFile -PathType Leaf)) { throw "Input file $($Arguments.InputFile) not found!" } if ($Arguments.InputFile -eq $Arguments.OutputFile) { throw 'Input file is identical to output file!' } if ($Arguments.OutputFile -notlike '*.exe' -and $Arguments.OutputFile -notlike '*.com') { throw "Output file must have extension '.exe' or '.com'!" } if ($Arguments.NoConsole -and $Arguments.ConHost) { throw '-NoConsole cannot be combined with -ConHost' } if ($Arguments.NoConsole -and $Arguments.TargetOS -and $Arguments.TargetOS -ne 'Windows') { throw '-NoConsole can only be used with -TargetOS Windows' } if ($Arguments.NoConsole -and ($IsLinux -or $IsMacOS)) { throw '-NoConsole is only supported on a Windows platform.' } if ($Arguments.RequireAdmin -and $Arguments.Virtualize) { throw '-RequireAdmin cannot be combined with -Virtualize' } if ($Arguments.SupportOS -and $Arguments.Virtualize) { throw '-SupportOS cannot be combined with -Virtualize' } if ($Arguments.LongPaths -and $Arguments.Virtualize) { throw '-LongPaths cannot be combined with -Virtualize' } if ($Arguments.STA -and $Arguments.MTA) { throw 'You cannot use switches -STA and -MTA at the same time!' } if ($Arguments.Core) { if (-not (Test-InstalledDependencies -VerifyDotnetTool)) { throw 'The .NET CLI (dotnet) is not installed or not in the system PATH.' } } if ($Arguments.PublishSingleFile -and $PSVersionTable.PSVersion -lt [Version]'7.6.0') { throw '-PublishSingleFile requires PowerShell 7.6.0 or higher because of a PowerShell bug.' } $isInstalled = $true if ($Arguments.TargetFramework -and -not $Arguments.PowerShellVersion) { $isInstalled = Test-InstalledDependencies -RequiredNetSdkVersion $Arguments.TargetFramework.Replace('net', '') } if ($Arguments.PowerShellVersion) { $versionMapping = Get-VersionMapping -PowerShellVersion $Arguments.PowerShellVersion if ($Arguments.TargetFramework) { if ($versionMapping.NetSdkVersion.Major -gt ([version]$Arguments.TargetFramework.Replace('net', '')).Major) { throw "The specified TargetFramework $($Arguments.TargetFramework) is not compatible with the specified PowerShellVersion $($Arguments.PowerShellVersion). The minimum required TargetFramework for PowerShell $($Arguments.PowerShellVersion) is net$($versionMapping.NetSdkVersion.Major)." } } $isInstalled = Test-InstalledDependencies -RequiredNetSdkVersion $versionMapping.NetSdkVersion } if (-not $isInstalled) { throw 'Not all dependencies are installed.' } } # file src\main\Private\Test-InstalledDependencies.ps1 <# .SYNOPSIS Tests if the required dependencies for building a .NET Core PowerShell Script with PS2EXE are installed. .DESCRIPTION This function checks if the necessary dependencies for building a .NET Core PowerShell script with PS2EXE are installed on the system. It verifies the presence of the .NET SDK and the required PowerShell version. .PARAMETER RequiredNetSdkVersion The required .NET SDK version that must be installed. .PARAMETER VerifyDotnetTool If specified, verifies that the .NET CLI (dotnet) is installed and available in the system PATH. .EXAMPLE PS> Test-InstalledDependencies This example checks if the required dependencies for building a .NET Core PowerShell script with PS2EXE are installed. .OUTPUTS System.Boolean Returns $true if all required dependencies are installed, otherwise returns $false. #> function Test-InstalledDependencies { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $false)] [version] $RequiredNetSdkVersion = '6.0.301', [Parameter(Mandatory = $false)] [switch] $VerifyDotnetTool ) if ($VerifyDotnetTool) { $command = Get-Command -Name dotnet -ErrorAction SilentlyContinue if ($null -eq $command) { return $false } return $true } # Check if .NET SDK is installed [array]$dotnetVersions = (dotnet --list-sdks 2>$null) -split "`n" $downloadLinkBase = "https://dotnet.microsoft.com/en-us/download" if ($dotnetVersions.Count -eq 0) { Write-Error "The .NET SDK is not installed. Please install it from $downloadLinkBase." return $false } $versionsOnSameMajor = $dotnetVersions | Where-Object { $_ -like "$($RequiredNetSdkVersion.Major).*" } if ($versionsOnSameMajor.Count -eq 0) { Write-Error "The installed .NET SDK versions ($($dotnetVersions -join ', ')) do not match the required version ($RequiredNetSdkVersion). Please install it from $downloadLinkBase/$($RequiredNetSdkVersion.Major).0." return $false } # Check if PowerShell version is sufficient if ($PSVersionTable.PSVersion -lt [version]'7.0') { Write-Error "PowerShell version 7.0 or higher is required. Current version: $($PSVersionTable.PSVersion)." return $false } Write-Verbose -Message "All required dependencies are installed." return $true } # file src\main\Private\Test-RequiresWinForms.ps1 <# .SYNOPSIS Tests if the script requires the System.Windows.Forms assembly. .DESCRIPTION This function checks if the System.Windows.Forms assembly is loaded and available for use in the script. .PARAMETER FilePath The path to the PowerShell script file to be tested. .EXAMPLE Test-RequiresWinForms -FilePath "C:\Scripts\Test.ps1" This command tests the specified PowerShell script file for the required System.Windows.Forms assembly. .OUTPUTS System.Boolean. True if the assembly is required, False otherwise. #> function Test-RequiresWinForms { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $FilePath ) if (-not (Test-Path -Path $FilePath)) { Write-Error "The file '$FilePath' does not exist." return $false } # Use the AST to check for issues $tokens = @() $errors = @() $null = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$tokens, [ref]$errors) if ($errors.Count -gt 0) { throw "Errors found in script '$FilePath':`n $($errors -join "`n")" } $requiresWinForms = $false if ($tokens.Value -contains "System.Windows.Forms") { if ($IsLinux -or $IsMacOS) { throw "The script requires System.Windows.Forms, which is not supported on Linux or macOS." } $requiresWinForms = $true if ($tokens.Value -contains "Write-Output" -or $tokens.Value -contains "Write-Host") { Write-Warning -Message "Script uses Write-Output and/or Write-Host, which will not work as expected in a WinForms application. WinForms applications do not have a console output." } } return $requiresWinForms } # file src\main\Private\Test-ScriptFile.ps1 <# .SYNOPSIS Tests the input file for syntax and semantic errors. .DESCRIPTION This function analyzes the PowerShell script file for common issues and provides feedback. .PARAMETER FilePath The path to the PowerShell script file to be tested. .EXAMPLE Test-ScriptFile -FilePath "C:\Scripts\Test.ps1" This command tests the specified PowerShell script file for syntax and semantic errors. .OUTPUTS System.Boolean. True if the script is valid, False otherwise. #> function Test-ScriptFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $FilePath ) if (-not (Test-Path -Path $FilePath)) { Write-Error "The file '$FilePath' does not exist." return $false } # Use the AST to check for issues $tokens = @() $errors = @() $null = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$tokens, [ref]$errors) if ($errors.Count -gt 0) { Write-Error "Errors found in script '$FilePath':`n $($errors -join "`n")" return $false } else { Write-Verbose -Message "No issues found in script '$FilePath'." } return $true } # file src\main\Public\Invoke-PS2EXE.ps1 #Requires -Version 3.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 Win-PS2EXE 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". A generated executable has the following reserved parameters: -? [<MODIFIER>] Powershell help text of the script inside the executable. The optional parameter combination "-? -detailed", "-? -examples" or "-? -full" can be used to get the appropriate help text. -debug Forces the executable to be debugged. It calls "System.Diagnostics.Debugger.Launch()". -extract:<FILENAME> Extracts the powerShell script inside the executable and saves it as FILENAME. The script will not be executed. -wait At the end of the script execution it writes "Hit any key to exit..." and waits for a key to be pressed. -end All following options will be passed to the script inside the executable. All preceding options are used by the executable itself. .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 x86 Compile for 32-bit runtime only. .PARAMETER x64 Compile for 64-bit runtime only. .PARAMETER ARM Compile for ARM architecture. This is only applicable when compiling for .NET Core. .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. Important: Disables redirection of input, output or error channel! .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 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 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 Core Use the dotnet CLI to compile the script to an executable instead of using the C# compiler built into Windows PowerShell. Note that this option will not produce a single executable file, but a folder with the executable and its dependencies. .PARAMETER TargetOS The target operating system for the executable. This is only applicable when compiling for .NET Core. Valid values are 'Windows', 'Linux', 'MacOS' and their ARM variants. Defaults to 'Windows'. .PARAMETER TargetFramework The target framework for the executable. This is only applicable when compiling for .NET Core. Valid values are 'net6.0', 'net7.0', 'net8.0' and 'net9.0'. Defaults to 'net9.0'. .PARAMETER PowerShellVersion The minimum version of PowerShell Core. Defaults to '7.5.2'. .PARAMETER SelfContained If this switch is set, the resulting executable will be self-contained and include the .NET runtime. This is only applicable when compiling for .NET Core. Increases the size of the executable significantly. .PARAMETER PublishSingleFile If this switch is set, the resulting executable will be published as a single file. This is only applicable when compiling for .NET Core. Note that this may not work with all scripts, especially those that rely on external files or resources. External files will not be included in the single file executable. .EXAMPLE PS> Invoke-PS2EXE C:\Data\MyScript.ps1 Compiles C:\Data\MyScript.ps1 to C:\Data\MyScript.exe as console executable. .EXAMPLE PS> 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 PS> Win-PS2EXE Start graphical front end to Invoke-PS2EXE. .LINK https://github.com/FabienTschanz/PS2EXE.Core .OUTPUTS None. #> function Invoke-PS2EXE { [CmdletBinding(DefaultParameterSetName = 'WinPS')] [Alias('ps2exe')] [Alias('ps2exe.ps1')] param( [Parameter(Mandatory = $true, Position = 0)] [System.String] $InputFile, [Parameter(Mandatory = $false, Position = 1)] [System.String] $OutputFile, [Parameter()] [switch] $PrepareDebug, [Parameter(ParameterSetName = 'WinPS')] [switch] $x86, [Parameter()] [switch] $x64, [Parameter(ParameterSetName = 'Core')] [switch] $ARM, [Parameter()] [int] $lcid, [Parameter(ParameterSetName = 'STA')] [Parameter(ParameterSetName = 'WinPS')] [Parameter(ParameterSetName = 'Core')] [switch] $STA, [Parameter(ParameterSetName = 'MTA')] [Parameter(ParameterSetName = 'WinPS')] [Parameter(ParameterSetName = 'Core')] [switch] $MTA, [Parameter()] [switch] $Nested, [Parameter(ParameterSetName = 'WinPS')] [Parameter(ParameterSetName = 'Core')] [switch] $NoConsole, [Parameter(ParameterSetName = 'WinPS')] [switch] $ConHost, [Parameter()] [switch] $UnicodeEncoding, [Parameter(ParameterSetName = 'WinPS')] [switch] $CredentialGUI, [Parameter()] [System.String] $IconFile, [Parameter()] [System.String] $Title, [Parameter()] [System.String] $Description, [Parameter()] [System.String] $Company, [Parameter()] [System.String] $Product, [Parameter()] [System.String] $Copyright, [Parameter()] [System.String] $Trademark, [Parameter()] [System.String] $Version = '0.0.0.0', [Parameter()] [switch] $ConfigFile, [Parameter()] [switch] $NoOutput, [Parameter()] [switch] $NoError, [Parameter()] [switch] $NoVisualStyles, [Parameter()] [switch] $ExitOnCancel, [Parameter()] [switch] $DPIAware, [Parameter(ParameterSetName = 'WinPS')] [switch] $WinFormsDPIAware, [Parameter()] [switch] $RequireAdmin, [Parameter()] [switch] $SupportOS, [Parameter()] [switch] $Virtualize, [Parameter()] [switch] $LongPaths, [Parameter(ParameterSetName = 'Core')] [switch] $Core, [Parameter(ParameterSetName = 'Core')] [ValidateSet('Windows', 'Linux', 'MacOS')] [System.String] $TargetOS = 'Windows', [Parameter(ParameterSetName = 'Core')] [ValidateSet('net6.0', 'net7.0', 'net8.0', 'net9.0')] [System.String] $TargetFramework, [Parameter(ParameterSetName = 'Core')] [ValidateSet('7.2.5', '7.2.8', '7.3.4', '7.3.5', '7.4.2', '7.4.5', '7.5.0', '7.5.1', '7.5.2')] [System.Version] $PowerShellVersion, [Parameter(ParameterSetName = 'Core')] [switch] $SelfContained, [Parameter(ParameterSetName = 'Core')] [switch] $PublishSingleFile ) if (-not $Nested) { Write-Output "PS2EXE-GUI v0.5.0.32 by Ingo Karstein, reworked and GUI support by Markus Scholtes, updated for PowerShell Core by Fabien Tschanz`n" } else { Write-Output "PowerShell Desktop environment started...`n" } Test-Arguments -Arguments $PSBoundParameters # Start Windows PowerShell if target is not Core if (-not $Nested -and ($PSVersionTable.PSEdition -eq 'Core') -and -not $Core -and $IsWindows) { $callParam = '' foreach ($Param in $PSBoundparameters.GetEnumerator()) { if ($Param.Value -is [System.Management.Automation.SwitchParameter]) { if ($Param.Value.IsPresent) { $callParam += " -$($Param.Key):`$true" } else { $callParam += " -$($Param.Key):`$false" } } else { if ($Param.Value -is [System.String]) { if (($Param.Value -match ' ') -or ([System.String]::IsNullOrEmpty($Param.Value))) { $callParam += " -$($Param.Key) '$($Param.Value)'" } else { $callParam += " -$($Param.Key) $($Param.Value)" } } else { $callParam += " -$($Param.Key) $($Param.Value)" } } } $callParam += ' -nested' powershell -Command "if ((Get-Command -Name 'Invoke-PS2EXE' -ErrorAction 'SilentlyContinue').Length -eq 0) { Import-Module '$PSScriptRoot\PS2EXE.Core.psm1' }; &'$($MyInvocation.MyCommand.Name)' $callParam" return } if ($PSCmdlet.ParameterSetName -eq 'Core') { if (-not $PSBoundParameters.ContainsKey('PowerShellVersion')) { $latestVersionCombination = Get-LatestVersionCombination if ($null -eq $latestVersionCombination) { throw 'Failed to automatically determine the PowerShell and .NET SDK version combination. Please specify it manually.' } $PowerShellVersion = $latestVersionCombination.PowerShellVersion $TargetFramework = "net$($latestVersionCombination.NetSdkVersion.Major).0" } if (-not $PSBoundParameters.ContainsKey('TargetFramework')) { $TargetFramework = "net$((Get-VersionMapping -PowerShellVersion $PowerShellVersion).NetSdkVersion.Major).0" } if (-not $PSBoundParameters.ContainsKey('TargetOS')) { if ($IsLinux) { $TargetOS = 'Linux' } elseif ($IsMacOS) { $TargetOS = 'MacOS' } else { $TargetOS = 'Windows' } } Write-Output "Determined PowerShell version $PowerShellVersion and .NET SDK version $TargetFramework. Target Platform is $TargetOS." } # Retrieve absolute paths independent if path is given relative oder absolute $InputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InputFile) if ([System.String]::IsNullOrEmpty($OutputFile)) { $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 -LiteralPath $OutputFile -PathType Container)) { $OutputFile = ([System.IO.Path]::Combine($OutputFile, [System.IO.Path]::GetFileNameWithoutExtension($InputFile) + '.exe')) } } if (-not [System.String]::IsNullOrEmpty($IconFile)) { # retrieve absolute path independent if path is given relative oder absolute $IconFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($IconFile) if (-not (Test-Path -LiteralPath $IconFile -PathType Leaf)) { throw "Icon file $($IconFile) not found!" } } if ($WinFormsDPIAware) { $SupportOS = $true } if (-not $ConfigFile -and $LongPaths) { Write-Warning 'Forcing generation of a config file, since the option -longPaths requires this' $ConfigFile = $true } if (-not $ConfigFile -and $WinFormsDPIAware) { Write-Warning 'Forcing generation of a config file, since the option -winFormsDPIAware requires this' $ConfigFile = $true } if (-not $MTA -and -not $STA) { # Set default apartment mode for powershell version if not set by parameter $STA = $true } # escape escape sequences in version info $Title = $Title -replace '\\', '\\' $Product = $Product -replace '\\', '\\' $Copyright = $Copyright -replace '\\', '\\' $Trademark = $Trademark -replace '\\', '\\' $Description = $Description -replace '\\', '\\' $Company = $Company -replace '\\', '\\' if (-not [System.String]::IsNullOrEmpty($Version)) { # check for correct version number information if ($Version -notmatch '(^\d+\.\d+\.\d+\.\d+$)|(^\d+\.\d+\.\d+$)|(^\d+\.\d+$)|(^\d+$)') { Write-Error 'Version number has to be supplied in the form n.n.n.n, n.n.n, n.n or n (with n as number)!' return } } Write-Output '' $o = [System.Collections.Generic.Dictionary[System.String, System.String]]::new() $o.Add('CompilerVersion', 'v4.0') $referenceAssembies = @('System.dll') if (-not $NoConsole) { if ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'Microsoft.PowerShell.ConsoleHost.dll' }) { $referenceAssembies += ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'Microsoft.PowerShell.ConsoleHost.dll' } | Select-Object -First 1).Location } } $referenceAssembies += ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'System.Management.Automation.dll' } | Select-Object -First 1).Location $n = New-Object System.Reflection.AssemblyName('System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') [System.AppDomain]::CurrentDomain.Load($n) | Out-Null $referenceAssembies += ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'System.Core.dll' } | Select-Object -First 1).Location if ($NoConsole) { $n = New-Object System.Reflection.AssemblyName('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089') [System.AppDomain]::CurrentDomain.Load($n) | Out-Null $n = New-Object System.Reflection.AssemblyName('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') [System.AppDomain]::CurrentDomain.Load($n) | Out-Null $referenceAssembies += ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'System.Windows.Forms.dll' } | Select-Object -First 1).Location $referenceAssembies += ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.ManifestModule.Name -ieq 'System.Drawing.dll' } | Select-Object -First 1).Location } $platform = 'anycpu' if ($x64 -and -not $x86) { $platform = 'x64' } elseif ($x86 -and -not $x64) { $platform = 'x86' } $codeProvider = (New-Object Microsoft.CSharp.CSharpCodeProvider($o)) $compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters($referenceAssembies, $OutputFile) $compilerParameters.GenerateInMemory = $false $compilerParameters.GenerateExecutable = $true $iconFileParam = '' if (-not ([System.String]::IsNullOrEmpty($IconFile))) { $iconFileParam = "`"/win32icon:$($IconFile)`"" } $manifestParam = '' if ($RequireAdmin -or $DPIAware -or $SupportOS -or $LongPaths) { $manifestParam = "`"/win32manifest:$($OutputFile+'.win32manifest')`"" $win32manifest = "<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>`r`n<assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0"">`r`n" if ($DPIAware -or $LongPaths) { $win32manifest += "<application xmlns=""urn:schemas-microsoft-com:asm.v3"">`r`n<windowsSettings>`r`n" if ($DPIAware) { $win32manifest += "<dpiAware xmlns=""http://schemas.microsoft.com/SMI/2005/WindowsSettings"">true</dpiAware>`r`n<dpiAwareness xmlns=""http://schemas.microsoft.com/SMI/2016/WindowsSettings"">PerMonitorV2</dpiAwareness>`r`n" } if ($LongPaths) { $win32manifest += "<longPathAware xmlns=""http://schemas.microsoft.com/SMI/2016/WindowsSettings"">true</longPathAware>`r`n" } $win32manifest += "</windowsSettings>`r`n</application>`r`n" } if ($RequireAdmin) { $win32manifest += "<trustInfo xmlns=""urn:schemas-microsoft-com:asm.v2"">`r`n<security>`r`n<requestedPrivileges xmlns=""urn:schemas-microsoft-com:asm.v3"">`r`n<requestedExecutionLevel level=""requireAdministrator"" uiAccess=""false""/>`r`n</requestedPrivileges>`r`n</security>`r`n</trustInfo>`r`n" } if ($SupportOS) { $win32manifest += "<compatibility xmlns=""urn:schemas-microsoft-com:compatibility.v1"">`r`n<application>`r`n<supportedOS Id=""{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}""/>`r`n<supportedOS Id=""{1f676c76-80e1-4239-95bb-83d0f6d0da78}""/>`r`n<supportedOS Id=""{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}""/>`r`n<supportedOS Id=""{35138b9a-5d96-4fbd-8e2d-a2440225f93a}""/>`r`n<supportedOS Id=""{e2011457-1546-43c5-a5fe-008deee3d3f0}""/>`r`n</application>`r`n</compatibility>`r`n" } $win32manifest += '</assembly>' $win32manifest | Set-Content ($OutputFile + '.win32manifest') -Encoding UTF8 } if (-not $Virtualize) { $compilerParameters.CompilerOptions = "/platform:$($platform) /target:$( if ($NoConsole -or $ConHost) { 'winexe' } else { 'exe' }) $($iconFileParam) $($manifestParam)" } else { Write-Output 'Application virtualization is activated, forcing x86 platform.' $compilerParameters.CompilerOptions = "/platform:x86 /target:$( if ($NoConsole -or $ConHost) { 'winexe' } else { 'exe' } ) /nowin32manifest $($iconFileParam)" } $compilerParameters.IncludeDebugInformation = $prepareDebug if ($prepareDebug) { $compilerParameters.TempFiles.KeepFiles = $true } Write-Output "Reading input file $InputFile" if (-not (Test-ScriptFile -FilePath $InputFile)) { throw "Input file $InputFile contains errors, aborting compilation!" } [void]$compilerParameters.EmbeddedResources.Add($InputFile) $mainCsPath = Join-Path -Path $PSScriptRoot 'main.cs' $programFrame = Get-Content -Path $mainCsPath -Raw -Encoding UTF8 if ($lcid) { $programFrame = $programFrame -replace "{{lcid}}", $lcid } $compilerDefinitions = @('NoConsole', 'CredentialGUI', 'MTA', 'STA', 'NoVisualStyles', 'WinFormsDPIAware', 'Title', 'NoError', 'NoOutput', 'ConHost', 'UnicodeEncoding', 'ExitOnCancel', 'lcid') $parameterDefinitions = "" $constants = @() foreach ($param in $compilerDefinitions) { if (Get-Variable -Name $param -ValueOnly) { $parameterDefinitions += " /define:$param" $constants += $param } } $programFrame = $programFrame -replace "{{Title}}", $Title $programFrame = $programFrame -replace "{{Product}}", $Product $programFrame = $programFrame -replace "{{Copyright}}", $Copyright $programFrame = $programFrame -replace "{{Trademark}}", $Trademark $programFrame = $programFrame -replace "{{Description}}", $Description $programFrame = $programFrame -replace "{{Company}}", $Company $programFrame = $programFrame -replace "{{Culture}}", $culture $programFrame = $programFrame -replace "{{FileName}}", [System.IO.Path]::GetFileName($InputFile) $programFrame = $programFrame -replace "{{Version}}", $Version if ($WinFormsDPIAware) { $ConfigFileForEXE3 = "<?xml version=""1.0"" encoding=""utf-8"" ?>`r`n<configuration><startup><supportedRuntime version=""v4.0"" sku="".NETFramework,Version=v4.7"" /></startup>" } else { $ConfigFileForEXE3 = "<?xml version=""1.0"" encoding=""utf-8"" ?>`r`n<configuration><startup><supportedRuntime version=""v4.0"" sku="".NETFramework,Version=v4.0"" /></startup>" } if ($LongPaths) { $ConfigFileForEXE3 += '<runtime><AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false" /></runtime>' } if ($WinFormsDPIAware) { $ConfigFileForEXE3 += '<System.Windows.Forms.ApplicationConfigurationSection><add key="DpiAwareness" value="PerMonitorV2" /></System.Windows.Forms.ApplicationConfigurationSection>' } $ConfigFileForEXE3 += '</configuration>' $baseCsPath = Join-Path -Path $PSScriptRoot 'base.csproj' $csProjFile = Get-Content -Path $baseCsPath -Raw -Encoding UTF8 Write-Output "Compiling file...`n" if ($Core) { [System.Environment]::SetEnvironmentVariable('DOTNET_CLI_TELEMETRY_OPTOUT', '1', 'User') $outputDirectory = [System.IO.Path]::GetDirectoryName($OutputFile) $outputFileName = [System.IO.Path]::GetFileNameWithoutExtension($OutputFile) $programFrame = $programFrame -replace "{{ResourcePrefix}}", "$outputFileName." $programFrame = Remove-EmptyPlaceholders -Content $programFrame New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null $scriptFilePath = [System.IO.Path]::Combine($outputDirectory, [System.IO.Path]::GetFileName($InputFile)) Copy-Item -Path $InputFile -Destination $scriptFilePath -Force $scriptCsPath = [System.IO.Path]::Combine($outputDirectory, "$($outputFileName).cs") $programFrame | Set-Content -Path $scriptCsPath -Encoding UTF8 $csProjPath = [System.IO.Path]::Combine($outputDirectory, "$($outputFileName).csproj") $runtimeIdentifier = 'win-x64' if ($TargetOS -eq 'Windows') { if ($x64) { $runtimeIdentifier = 'win-x64' } elseif ($x86) { $runtimeIdentifier = 'win-x86' } elseif ($ARM) { $runtimeIdentifier = 'win-arm64' } } elseif ($TargetOS -eq 'Linux') { if ($ARM) { $runtimeIdentifier = 'linux-arm64' } else { $runtimeIdentifier = 'linux-x64' } } elseif ($TargetOS -eq 'MacOS') { if ($ARM) { $runtimeIdentifier = 'osx-arm64' } else { $runtimeIdentifier = 'osx-x64' } } $requiresWinForms = Test-RequiresWinForms -FilePath $InputFile $csProjFile = $csProjFile -replace "{{InputFile}}", ([System.IO.Path]::GetFileName($InputFile)) $csProjFile = $csProjFile -replace "{{RuntimeIdentifier}}", $runtimeIdentifier $csProjFile = $csProjFile -replace "{{TargetFramework}}", $TargetFramework $csProjFile = $csProjFile -replace "{{PowerShellVersion}}", $PowerShellVersion $csProjFile = $csProjFile -replace "{{SelfContained}}", $SelfContained $csProjFile = $csProjFile -replace "{{PublishSingleFile}}", $PublishSingleFile $csProjFile = $csProjFile -replace "{{UseWindowsForms}}", ($NoConsole -or $CredentialGUI -or $requiresWinForms) $csProjFile = $csProjFile -replace "{{DefineConstants}}", $constants -join ';' if ($NoConsole -or $requiresWinForms) { $csProjFile = $csProjFile -replace "{{OutputType}}", 'WinExe' $csProjFile = $csProjFile -replace "{{TargetOS}}", '-windows' } else { $csProjFile = $csProjFile -replace "{{OutputType}}", 'Exe' } $csProjFile = Remove-EmptyPlaceholders -Content $csProjFile $csProjFile | Set-Content -Path $csProjPath -Encoding UTF8 $outputDirectory = Join-Path -Path $outputDirectory -ChildPath 'Release' if ($VerbosePreference -eq 'Continue') { dotnet publish $csProjPath -c Release -o $outputDirectory -v d } else { dotnet publish $csProjPath -c Release -o $outputDirectory -v q --property WarningLevel=0 /clp:ErrorsOnly } if ($LASTEXITCODE -ne 0) { throw "dotnet build failed with exit code $LASTEXITCODE" } Write-Output "Successfully built project in $outputDirectory." if (-not $PSBoundParameters.ContainsKey('PublishSingleFile')) { Write-Output "If you want to package the executable into a single file, use the -PublishSingleFile parameter. This only works with PowerShell 7.6.0 or higher." } Remove-Item -Path $csProjPath -Force -ErrorAction SilentlyContinue Remove-Item -Path $scriptCsPath -Force -ErrorAction SilentlyContinue } else { $programFrame = Remove-EmptyPlaceholders -Content $programFrame $compilerParameters.CompilerOptions += $parameterDefinitions $cr = $codeProvider.CompileAssemblyFromSource($compilerParameters, $programFrame) if ($cr.Errors.Count -gt 0) { if (Test-Path -LiteralPath $OutputFile) { Remove-Item -LiteralPath $OutputFile -Verbose:$false } Write-Error -ErrorAction Continue 'Could not create the PowerShell .exe file because of compilation errors. Use -verbose parameter to see details.' $cr.Errors | ForEach-Object { Write-Verbose $_ } } else { if (Test-Path -LiteralPath $OutputFile) { Write-Output "Output file $OutputFile written" 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-Output "Source file name for debug copied: $($dstSrc)" Copy-Item -Path $_ -Destination $dstSrc -Force } $cr.TempFiles | Remove-Item -Verbose:$false -Force -ErrorAction SilentlyContinue } if ($ConfigFile) { $ConfigFileForEXE3 | Set-Content ($OutputFile + '.config') -Encoding UTF8 Write-Output 'Config file for EXE created' } } else { Write-Error -ErrorAction 'Continue' "Output file $OutputFile not written" } } } if ($RequireAdmin -or $DPIAware -or $SupportOS -or $LongPaths) { if (Test-Path -LiteralPath $($OutputFile + '.win32manifest')) { Remove-Item -LiteralPath $($OutputFile + '.win32manifest') -Verbose:$false } } } # Define aliases Set-Alias -Name "Win-PS2EXE" -Value "$PSScriptRoot\Win-PS2EXE.exe" -Scope Global Set-Alias -Name "Win-PS2EXE.exe" -Value "$PSScriptRoot\Win-PS2EXE.exe" -Scope Global $ErrorActionPreference = 'Stop' # Load Module variables file try { $variables = Get-Content "$PSScriptRoot\Variables.json" -Raw | ConvertFrom-Json foreach ($variable in $variables) { New-Variable -Name $variable.Name -Value $variable.Value -Scope Script -Option Constant } } catch { throw 'Could not load Variables.json file.' } |