Scroll Top
Evotec Services sp. z o.o., ul. Drozdów 6, Mikołów, 43-190, Poland

Making PowerShellGallery modules Portable

img_5d742614ce535

I'm a big fan of PowerShellGallery. It's easy to use, Microsoft owned, a place to host your PowerShell modules. Every time I release a new PowerShell module, it's readily available for me or anyone with a single command Install-Module. No need to host it yourself, no need to prepare anything – plug & play. Additionally, if your PowerShell module has any dependencies, it will download and install them, so it directly works out of the box. But what if you can't use PowerShellGallery? What if you don't want to use Install-Module on 100 computers, but you prefer to do it in a more controlled way? What if your servers do not have internet connectivity?

Making PowerShellGallery modules Portable

I usually would use GitHub sources and build it from that. This seems relatively easy till you find out your PowerShell module has seven other dependencies that you need to download and use. Additionally, if you download my modules from GitHub, they don't have any optimization that I usually do when publishing them to PowerShellGallery. Fortunately, there's a way to make PowerShellGallery modules portable. Below is a simple function that when given Module Name will download the module we requested with all it's dependencies to the specified folder. After that, it will import all those modules to memory, making it available to use.

function Initialize-ModulePortable {
    [CmdletBinding()]
    param(
        [alias('ModuleName')][string] $Name,
        [string] $Path = $PSScriptRoot,
        [switch] $Download,
        [switch] $Import
    )
    function Get-RequiredModule {
        param(
            [string] $Path,
            [string] $Name
        )
        $PrimaryModule = Get-ChildItem -LiteralPath "$Path\$Name" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1
        if ($PrimaryModule) {
            $Module = Get-Module -ListAvailable $PrimaryModule.FullName -ErrorAction SilentlyContinue -Verbose:$false
            if ($Module) {
                [Array] $RequiredModules = $Module.RequiredModules.Name
                if ($null -ne $RequiredModules) {
                    $null
                }
                $RequiredModules
                foreach ($_ in $RequiredModules) {
                    Get-RequiredModule -Path $Path -Name $_
                }
            }
        } else {
            Write-Warning "Initialize-ModulePortable - Modules to load not found in $Path"
        }
    }

    if (-not $Name) {
        Write-Warning "Initialize-ModulePortable - Module name not given. Terminating."
        return
    }
    if (-not $Download -and -not $Import) {
        Write-Warning "Initialize-ModulePortable - Please choose Download/Import switch. Terminating."
        return
    }

    if ($Download) {
        try {
            if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
                $null = New-Item -ItemType Directory -Path $Path -Force
            }
            Save-Module -Name $Name -LiteralPath $Path -WarningVariable WarningData -WarningAction SilentlyContinue -ErrorAction Stop
        } catch {
            $ErrorMessage = $_.Exception.Message

            if ($WarningData) {
                Write-Warning "Initialize-ModulePortable - $WarningData"
            }
            Write-Warning "Initialize-ModulePortable - Error $ErrorMessage"
            return
        }
    }

    if ($Download -or $Import) {
        [Array] $Modules = Get-RequiredModule -Path $Path -Name $Name | Where-Object { $null -ne $_ }
        if ($null -ne $Modules) {
            [array]::Reverse($Modules)
        }
        $CleanedModules = [System.Collections.Generic.List[string]]::new()

        foreach ($_ in $Modules) {
            if ($CleanedModules -notcontains $_) {
                $CleanedModules.Add($_)
            }
        }
        $CleanedModules.Add($Name)

        $Items = foreach ($_ in $CleanedModules) {
            Get-ChildItem -LiteralPath "$Path\$_" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1
        }
        [Array] $PSD1Files = $Items.FullName
    }
    if ($Download) {
        $ListFiles = foreach ($PSD1 in $PSD1Files) {
            $PSD1.Replace("$Path", '$PSScriptRoot')
        }
        # Build File
        $Content = @(
            '$Modules = @('
            foreach ($_ in $ListFiles) {
                "   `"$_`""
            }
            ')'
            "foreach (`$_ in `$Modules) {"
            "   Import-Module `$_ -Verbose:`$false -Force"
            "}"
        )
        $Content | Set-Content -Path $Path\$Name.ps1 -Force
    }
    if ($Import) {
        $ListFiles = foreach ($PSD1 in $PSD1Files) {
            $PSD1
        }
        foreach ($_ in $ListFiles) {
            Import-Module $_ -Verbose:$false -Force
        }
    }
}

It's effortless but at the same time very useful. Updating your disconnected environment is much easier this way. You can quickly test something and remove a folder from your machine without having it installed.

Initialize-ModulePortable -Name 'Testimo' -Download -Path $PSScriptRoot

As you can see on the screenshot above it looks like the command is Installing the module, but it's using Save-Module to download module to a given directory. After that, it creates a new PS1 file and puts it into the primary folder. The file name is identical to the module name. The file has logic to read module PSD1 file, check for RequiredModules section and import them in the order given in PSD1. Finally, script imports requested module.

If we check directory, we can see it downloaded nine modules to that folder. You can also see the file named Testimo.ps1 which, when executed, will Import all those modules in hopefully proper order. That means you can then copy that folder anywhere, execute with PS1 file that does all the hard work for you. After that all commands become available and you can use them as you would do after standard installation. Keep in mind that I've tested this on limited number of modules. It's possible I've skipped something when writting this. Current development version is located on GitHUB as part of PSSharedGoods module. Feel free to submit bugs if you find one.

Related Posts