PowerShell Pro Tools is capable of bundling assets along with your PowerShell executables. In addition to being able to bundle the scripts themselves, you can also include modules, binaries and other resources.

Bundling Other Scripts

When the bundler is enabled, it will automatically bundle other scripts. If you dot-source scripts, they will be bundled into the main script. If those scripts dot-source scripts, those scripts will be included and so forth. You can use bundling without the packager if you want to combine many scripts into a single script. To enable bundling, you can turn on the bundling flag in your package.psd1 file or your PowerShell Tools for Visual Studio project file.

Assume I have a script that dot-sources another script.

Start-Process Notepad
.\GetProcess.ps1

The GetProcess.ps1 looks like this.

Get-Process notepad

The resulting script will be:

Start-Process Notepad
Get-Process notepad

You can enable this functionality in different ways depending on which set of tools you are using. The bundler process is the same but the configuration is different.

In Visual Studio Code or using the PowerShellProTools module, you can use a package.psd1 file to configure the bundling and packaging settings. This is an example package.psd1 file that would bundle the scripts together. To start the bundler in Visual Studio Code, execute the Package as Executable command. On the command line, use Merge-Script.

@{
    Root = 'C:\Bundle.ps1'
    OutputPath = 'C:\out'
    Bundle = @{
        Enabled = $true
    }
}

In Visual Studio, you can use project settings to turn on debugging. Right-click on your project and click properties. In the advanced tab you can enable bundling by selecting the checkbox. To bundle your script, build the project. Output about the bundling process will be sent to the Output Pane.

Enabling Bundling in Visual Studio

Bundling Modules

Bundling modules only works when you are creating a packaged PowerShell script. To bundle modules, it’s as easy as enabling the module bundling flag in either your package.psd1 config or within your PowerShell Tools for Visual Studio project file. This will include any modules that you manually import with Import-Module. The bundler will not pick up modules that you auto-import so make sure to include an Import-Module call to the modules you would like to include.

Modules that you are bundling need to be discoverable by path or on the $Env:PSModulePath. The contents of the module folder will be included with the executable. When the executable is run, the module is then staged and imported into the PowerShell session so that it is available to the script to use.

Let’s assume that we have a module that is install in our user’s module folder. We have a script that utilizes the module. All we need to do is make sure that the script contains an Import-Module statement as well as enable the module bundling.

Import-Module ThreadJob 
1..10 | ForEach-Object { Start-ThreadJob -ScriptBlock { Start-Sleep 1 } } | Wait-Job

If we want to configure module bundling in Visual Studio Code or PowerShell, we can use the package.psd1.

@{
    Root = 'C:\Bundle.ps1'
    OutputPath = 'C:\out'
    Package = @{
        Enabled = $true
    }
    Bundle = @{
        Enabled = $true
        Modules = $true
    }
}

In Visual Studio, you can enable packaging and module bundling in the project settings.

Package as Executable Setting

Just as you did with the bundler, you can run the bundling and packaging process by invoking the Package as Executable command in VS Code, the Merge-Script cmdlet in PowerShell or build the project in Visual Studio.

You will see output in the various output windows that will provide info about the packaging process as well as list the output executable.

VS Code Output of Packaging

You will notice the size of the executable is larger than just the PS1 script alone.

Bundling Assemblies

You can also bundle .NET assemblies directly into your scripts. There is no setting you need to enable to make this happen; aside from enabling bundling. The bundling process will embed any assemblies that you load with Add-Type or Assembly.LoadFrom. After this process is complete, there is no need to include the assembly with the executable.

An example script could load a DLL as follows.

[System.Reflection.Assembly]::LoadFrom("$PSScriptRoot\classes.dll")
New-Object -TypeName classes.Class1

The bundle config is the same as if you were just bundling a script.

@{
    Root = 'C:\Script.ps1'
    OutputPath = 'C:\OutputScript.ps1'
    Bundle = @{
        Enabled = $true
    }
}

The resulting script will have the assembly embedded. You can also use this with packaging to produce an executable that has the assembly embedded.

Bundling and Packaging Caveats

Bundling and packaging can have some caveats. Due to the nature of bundling you may run into issues if you do not understand how the process is working.

$PSScriptRoot Support

The packaging process supports the $PSScriptRoot variable. During the packaging process it actually replaces all the instances of $PSScriptRoot with a different variable since $PSScriptRoot is set by PowerShell. The new variable is then set to the current location of the executable so it behaves just as you would imagine. This only happens during packaging and not during bundling.

Path Joining

Although many path joining techniques are supported by the bundler and packager, you may run into issues where scripts are not being bundled into the final product. This is because the bundler uses static analysis to determine the path to the files that are being bundled. If the bundler cannot locate the file, it will fail to bundle it and leave the execution as is. In this case, you may encounter issues running the final script or executable.

Some examples of supported path joining techniques include:

. (Join-Path $PSScriptRoot 'myScript.ps1')
. "$PSScriptRoot\myScript.ps1"

The use of other variables may cause issues during bundling because the bundler does not known the correct value when it is analyzing the script. This is an example of a script that will fail to bundle.

$MyRootPath = $PSScriptRoot 
. "$MyRootPath\myScript.ps1"

Other Resources

Other resources that you may use in your scripts will not be included. You will need to include them along side the executable. You can access the resources just as you would with a PowerShell script by taking advantage of $PSScriptRoot and loading the resource.