Compiling PowerShell Scripts into an Executable with Merge-Script

Image Description

Daily PowerShell #52

Daily PowerShell PowerShell Pro Tools

December 6, 2021

quote Discuss this Article

Merge-Script is a cmdlet from PowerShell Pro Tools that provides the ability to compile PowerShell scripts into executables.

How does Merge-Script compile PowerShell scripts?

Merge-Script takes advantage of the .NET SDK to embed PowerShell scripts into a highly customizable .NET application. The PowerShell script itself is not compiled but rather packaged and executed when the application is run.

Setting up your Environment

You will first need to install the PowerShell Pro Tools module. You will need a license key.

Install-Module PowerShellProTools

Once the module is installed, you will have access to the Merge-Script cmdlet. This cmdlet takes advantage of the .NET SDK. You will need to install the SDK and can do so with this script provided by Microsoft. This will install the LTS version (currently, 6.0).

Invoke-WebRequest https://dot.net/v1/dotnet-install.ps1 -OutFile .\dotnet-install.ps1
./dotnet-install.ps1 -Channel LTS

If you want to build executables for Windows PowerShell, you’ll need to also install a .NET Framework Developer Pack. You can use Chocolatey to do so.

choco install netfx-4.6.2-devpack

Compile a Binary

Now that your have your environment configured, you can compile a binary. Let’s assume you have a binary that prints to the command line.

Write-Host "Hello, World!"

To package the executable, you can use the following command line.

Merge-Script -Script .\HelloWorld.ps1 -Package -OutputPath .\

Once the executable is compiled, you can run it.

PS > .\HelloWorld.exe
Hello, World!

Compile a Binary with PowerShell 7

PowerShell 7 uses the .NET Core 3.1 or later. Depending on the version of PowerShell 7 you select, you will need to use the appropriate .NET SDK version.

This example will compile a PowerShell 7.1 assembly with .NET 5.0. You can customize the compiling process by providing a package configuration. The packaging configuration is provides many options and can be specified as a PSD1 file or hashtable.

Below is a basic example of a configuration file for producing a PowerShell 7.1 executable. You can save the file as package.psd1.

@{
   Root = "C:\users\adamr\desktop\HelloWorld\HelloWorld.ps1"
   OutputPath = "C:\users\adamr\desktop\HelloWorld\"
   Package = @{
     Enabled = $true 
     DotNetVersion = "net5.0"
     PowerShellVersion = "7.1.4"
   }
   Bundle = @{
     Enabled = $true
   }
}

You can then call Merge-Script and provide the package configuration path.

Merge-Script -ConfigFile .\package.psd1

You will notice that the executable for PowerShell 7 is much larger than for Windows PowerShell. PowerShell 7 executables contain the entire .NET runtime and PowerShell SDK. This means that they can be run on machines without having PowerShell 7 installed on them.

PS > (Get-ChildItem .\HelloWorld.exe).Length / 1MB
146.821546554565

The executable will work the same as the first executable.

PS > .\HelloWorld.exe
Hello, World!

Include Modules with the Executable

You can also include modules with your binary. Merge-Script will analyze the script to look for calls to Import-Module. Any module imported this way will be packaged with the executable. When you include modules in an executable, they do not need to be installed on the target machine.

For example, we could update the HelloWorld.ps1 script to include the PowerShell Pro Tools module.

Import-Module PowerShellProTools
Show-PSScriptPad

We’ll also update the package.psd1 file to include modules.

@{
   Root = "C:\users\adamr\desktop\HelloWorld\HelloWorld.ps1"
   OutputPath = "C:\users\adamr\desktop\HelloWorld\"
   Package = @{
     Enabled = $true 
     DotNetVersion = "net4.6.2"
     PowerShellVersion = "Windows PowerShell"
   }
   Bundle = @{
     Enabled = $true
     Modules = $true
   }
}

Launching the executable, in this case, will call Show-PSScriptPad and you will see the application start.

Anti-Virus

Sometimes, anti-virus may incorrectly flag your assemblies as malicious. We have developed some documentation to help work around this issue.