Build EXE in .net or .net core

A conversation started on another thread. This is a demo using PowerShellProTools cmdlet Merge-Script to package a ps1 as an executable.

This demo code contains two different configurations. The default configuration will build for .Net core 9.0/powershell core 7.5.0. Copy the contents to a ps1 file and call from either powershell.exe or pwsh.exe.

$Path = $env:TEMP # choose your own path, a folder where the script.ps1, config.psd1, and generated executable will be stored
$scriptfile = Join-Path $Path script.ps1
$configfile = Join-Path $Path package.psd1
$destination = 'google.com'
$psicon = '\path\to\your\icon.file'
$pscoreicon = 'C:\Program Files\PowerShell\7\assets\Powershell_black.ico'
$configexample = 1 # 0 for windows powershell, 1 for ps core

$ErrorActionPreference = 'Stop'

$modulelist = @'
    Name,Version
    PowershellProTools,2025.2.0
'@ | ConvertFrom-Csv

foreach($module in $modulelist){
    Write-Host "Verifying $($module.Name) module is installed" -ForegroundColor Cyan

    $installed = Get-Module -ListAvailable -Name $module.Name |
        Sort-Object -Property {[version]$_.version} | Select-Object -Last 1

    if(-not $installed -or [version]$installed.version -lt [version]$module.Version){
        Write-Host "Searching gallery for module $($module.Name)"

        $found = Find-Module -Name $module.Name -AllVersions |
                Sort-Object -Property {[version]$_.version} | Select-Object -Last 1

        Write-Host "Installing $($module.Name) $($found.Version)" -ForegroundColor Cyan
        $found | Install-Module -Force -Scope CurrentUser
    }
}

$scriptcontents = @'
Import-Module NetTCPIP -Force
Import-Module DnsClient -Force

$host.UI.RawUI.WindowTitle = '{0} pinger v1.0'

Write-Host "Pinger utility running in PS version $($PSVersionTable.PSVersion)" -ForegroundColor Cyan

Write-Host "Testing connection to {0}" -ForegroundColor Cyan

Test-NetCOnnection -ComputerName {0}

Write-Host "Completed connection test to {0}" -ForegroundColor Cyan

Write-Host "Press enter to continue" -ForegroundColor Cyan

Read-Host
'@ -f $destination

$configcontents = @(
# Powershell 5/.net 4.x
@"
@{
    Root = '$scriptfile'
    OutputPath = '$Path'
    Package = @{
        Enabled = `$true
        Obfuscate = `$false
        HideConsoleWindow = `$false
        DotNetVersion = 'v4.6.2'
        FileVersion = '1.0.4'
        FileDescription = 'Utility to ping $destination'
        Platform = 'x64'
        ProductName = '$destination pinger'
        ProductVersion = '2.2'
        Copyright = '© 2025 KrzyDoug'
        RequireElevation = `$false
        #ApplicationIconPath = '$((Get-Command powershell.exe).source)'
        PackageType = 'Console'
    }
    Bundle = @{
        Enabled = `$true
        Modules = `$true
        # IgnoredModules = @()
    }
}
"@

# Powershell core/.net core
@"
@{
    Root = '$scriptfile'
    OutputPath = '$Path'
    Package = @{
        Enabled = `$true
        Obfuscate = `$false
        HideConsoleWindow = `$false
        DotNetVersion = 'net9.0'
        FileVersion = '1.0.4'
        FileDescription = 'Utility to ping $destination'
        Platform = 'x64'
        PowerShellVersion = '7.5.0'
        ProductName = '$destination pinger'
        ProductVersion = '2.2'
        Copyright = '© 2025 KrzyDoug'
        RequireElevation = `$false
        ApplicationIconPath = '$pscoreicon'
        PackageType = 'Console'
    }
    Bundle = @{
        Enabled = `$true
        Modules = `$true
        # IgnoredModules = @()
    }
}
"@
)[$configexample]

try{
    Set-Content -LiteralPath $scriptfile -Value $scriptcontents
    Write-Host "$scriptfile created successfully" -ForegroundColor Cyan
}
catch{
    Write-Warning $_.exception.message
    Exit 1
}

try{
    Set-Content $configfile -Value $configcontents
    Write-Host "$configfile created successfully" -ForegroundColor Cyan
}
catch{
    Write-Warning $_.exception.message
    Exit 1
}

Write-Host "Packaging $scriptfile as exe file" -ForegroundColor Cyan

Start-Sleep -Seconds (Get-Random (2..4))

try{
    $outputfile = Merge-Script -ConfigFile $configfile
    Write-Host "Executable package $($outputfile.OutputFileName) created successfully" -ForegroundColor Cyan
}
catch{
    Write-Warning $_.exception.message
    Exit 1
}

Write-Host "Press enter to continue" -ForegroundColor Cyan

Read-Host
cmd /c "explorer.exe /select,$($outputfile.OutputFileName)"

It will generate a demo executable that just pings google.com.

After you press enter, the directory will open with the exe highlighted.
image

It’s a simple network test to google.com.

Notice the size, it’s over 200MB and that’s a sign it’s a .net core package that’s all self contained.

To build against .Net Framework/Powershell 5.1, change the line with $configexample to 0.

$configexample = 1 # 0 for windows powershell, 1 for ps core

Then when you run the script again, it will build the exe against .Net Framework.
image

The file size is much smaller, as the system is expected to have .Net Framework 4.6.2 or higher installed.

The basic demo tool is the same

1 Like