ModuleMgmt.psm1

<#
#>


########
# Global settings
$InformationPreference = "Continue"
$ErrorActionPreference = "Stop"
Set-StrictMode -Version 2

<#
#>

Function Use-PowerShellGallery
{
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory=$false)]
        [ValidateSet("AllUsers", "CurrentUser")]
        [string]$Scope = "CurrentUser"
    )

    process
    {
        Write-Verbose "Installing Nuget package provider"
        if ($PSCmdlet.ShouldProcess("Nuget Provider", "Update"))
        {
            Write-Verbose "Attempting nuget provider update"
            try {
                # Set TLS support to 1.1 and 1.2 explicitly
                [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -Scope $Scope
            } catch {
                Write-Warning "Couldn't install nuget package provider"
            }
        }

        Write-Verbose "Trusting PSGallery"
        if ($PSCmdlet.ShouldProcess("PSGallery Repository", "Trust"))
        {
            try {
                Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted
            } catch {
                Write-Verbose ("Set-PSRepository for PSGallery failed: " + $_)
            }
        }
    }
}

<#
#>

Function Install-PSModuleFromManifest
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$ManifestPath,

        [Parameter(Mandatory=$false)]
        [ValidateSet("AllUsers", "CurrentUser")]
        [string]$Scope = "CurrentUser",

        [Parameter(Mandatory=$false)]
        [switch]$Force = $false
    )

    process
    {
        Write-Verbose "Reading content from manifest: $ManifestPath"
        $content = Get-Content -Encoding UTF8 $ManifestPath | Out-String

        # Convert to a script block and execute
        Write-Verbose "Converting to manifest object"
        $block = [ScriptBlock]::Create($content)
        $meta = & $block | Select-Object -First 1

        # Check that we have received a hashtable from the manifest file
        if ($null -eq $meta -or $meta.GetType().FullName -ne "System.Collections.Hashtable")
        {
            Write-Error "Invalid type returned from manifest file"
            return
        }

        # Check if we have any required modules to install
        if ($meta.Keys -notcontains "RequiredModules")
        {
            Write-Verbose "No required modules to install or section missing"
            return
        }

        $meta["RequiredModules"] | ForEach-Object {
            $module = $_

            # Attempt best effort to continue if there is an invalid entry
            if ($null -eq $module -or $module.GetType().FullName -ne "System.Collections.Hashtable")
            {
                Write-Warning "Missing or invalid type in RequiredModules"
                return
            }

            # ModuleName is mandatory in the entry
            if ($module.Keys -notcontains "ModuleName")
            {
                Write-Warning "Missing module name in RequiredModules entry"
                return
            }

            # Translate keys from the RequiredModules entry to parameters for
            # Install-Module
            $installParams = @{
                Scope = $Scope
                Force = $Force
            }

            ("ModuleName", "Name"),
                ("RequiredVersion", "RequiredVersion"),
                ("MaximumVersion", "MaximumVersion"),
                ("ModuleVersion", "MinimumVersion") | ForEach-Object {
                    if ($module.Keys -contains $_[0])
                    {
                        $installParams[$_[1]] = $module[$_[0]]
                    }
                }

            # Install the module
            Write-Verbose ("Installing module: {0}" -f $installParams["Name"])
            if ($PSCmdlet.ShouldProcess($installParams["Name"], "Install"))
            {
                Install-Module @installParams
            }
        }
    }
}

<#
#>

Function Select-ModuleVersionMatch
{
    [OutputType("System.String")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Version,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Major = -1,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Minor = -1,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Patch = -1,

        [Parameter(Mandatory=$false)]
        [switch]$StrictParse = $false
    )

    process
    {
        try {
            Write-Verbose "Parsing version $Version"
            $parsed = [Version]::Parse($Version)

            if ($Major -ge 0 -and $parsed.Major -ne $Major)
            {
                return
            }

            if ($Minor -ge 0 -and $parsed.Minor -ne $Minor)
            {
                return
            }

            if ($Patch -ge 0 -and $parsed.Build -ne $Patch)
            {
                return
            }

            $Version
        } catch {
            if ($StrictParse)
            {
                Write-Error "Failed to parse version string: $Version"
            } else {
                Write-Warning "Failed to parse version string: $Version"
            }
        }
    }
}

<#
#>

Function Install-PSModuleWithSpec
{
    [CmdletBinding()]
    param(
        [Parameter(mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Major = -1,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Minor = -1,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [int]$Patch = -1,

        [Parameter(mandatory=$false)]
        [switch]$Offline = $false,

        [Parameter(Mandatory=$false)]
        [ValidateSet("CurrentUser", "AllUsers")]
        [string]$Scope = "CurrentUser"
    )

    process
    {
        # If not offline, install the latest version that matches the spec, if it doesn't exist locally
        if (!$Offline)
        {
            $target = $null
            try {
                # Get the modules available online and filter by version spec
                $target = Find-Module -AllVersions -Name $Name -EA Stop |
                    ForEach-Object { $_.Version.ToString() } |
                    Select-ModuleVersionMatch -Major $Major -Minor $Minor -Patch $Patch |
                    Select-Object -First 1
                Write-Verbose "Find module result: $target"
            } catch {
                Write-Warning "Failed to get online module info for $Name"
            }

            # Get the local modules
            $installed = Get-Module -ListAvailable -Name $Name | ForEach-Object { $_.Version.ToString() }
            Write-Verbose ("Found local modules: " + ($installed -join ","))

            # If we found a match online, check if it is installed locally
            if (![string]::IsNullOrEmpty($target) -and ($null -eq $installed -or $installed -notcontains $target))
            {
                Write-Verbose "Installing module $Name version $target"
                Install-Module -Name $Name -RequiredVersion $target -Force -SkipPublisherCheck -Scope $Scope
            }
        }

        # Get the local modules and filter by spec
        $target = Get-Module -ListAvailable -Name $Name |
            ForEach-Object { $_.Version.ToString() } |
            Select-ModuleVersionMatch -Major $Major -Minor $Minor -Patch $Patch |
            Select-Object -First 1

        if ($null -eq $target)
        {
            Write-Error "Could not find suitable installed version for $Name"
        } else {
            # Return matching target version
            Write-Verbose "Found local module to import: $target"
            $target
        }
    }
}