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.'
}