Public/Install-emsScriptModules.ps1

<#PSScriptInfo
 
.VERSION 1.0.0
 
.GUID 27a6e509-b2ac-4f24-b0c0-986ef5c59d0d
 
.AUTHOR enthus Managed Services GmbH
 
.COMPANYNAME enthus Managed Services GmbH
 
#>


#Requires -Version 5.1

<#
    .SYNOPSIS
    Installs PowerShell modules which are defined in a script file info of a specific file.

    .DESCRIPTION
    Installs PowerShell modules which are defined in a script file info of a specific file. Depending and interfering modules will be uninstalled at first.
 
    Runs with 'ErrorAction' set to 'Error' by default (unless 'ErrorAction' parameter is explicitly set).
 
    Hint: Module 'Microsoft.Graph.Authentication' must be declared before all other 'Microsoft.Graph' modules.

    .NOTES
    PSVersion: 5.1.x or 7.2.x

    ENVIRONMENT:
    [ ] Azure Automation
    [ ] Azure Function
    [x] Local
    [ ] Nerdio
    [ ] PowerShell Universal
    [ ] Server
    [ ] ...

    REQUIRED CONTEXT:
    [ ] Application && Delegated
    [ ] Application
    [ ] Delegated
    [x] User

    REQUIRED PERMISSIONS:
    Local:
    - Administrator
 
    .OUTPUTS
    None.
 
    .EXAMPLE
    Install-emsScriptModules -FilePath "C:\development\git\KUD-EMS-Test-O365\Runbooks\TestScript.ps1"
 
    Checks required modules of the given file, uninstalls all depending and interfering modules and installs all required modules.
#>

function Install-emsScriptModules {
    [CmdletBinding()]
    param(
        # string. Path to the file which contains required PowerShell modules to install
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath
    )
    # TODO 'Remove' active/used modules before uninstalling
    # TODO Handle modules that can not be found in public PowerShell gallery (e. g. 'Vindesk.Core'), for example by exclusion list

    # Handle ErrorActionPreference
    $inputErrorAction = $PSBoundParameters['ErrorAction']
    if (!$inputErrorAction) {
        $activeErrorActionPreference = $ErrorActionPreference
        $ErrorActionPreference = "Stop"
    }

    try {
        # Modules within one line must not be installed concurrently
        $interferingModuleNamesLists = @(
            @("PnP.PowerShell", "SharePointPnPPowerShellOnline"),
            @("AzureAD", "AzureADPreview"),
            @() # Workaround to force PowerShell to iterate over inner arrays
        )

        # TODO Extend object with attributes "ModuleBaseName" and "DependencyModuleName" (for module 'Microsoft.Graph.Authentication')
        $dependingModuleNames = @(
            "Microsoft.Graph"
        )

        $requiredModules = (Test-ScriptFileInfo -Path $FilePath).RequiredModules

        if ($requiredModules) {
            $interferingModuleNames = @()
            foreach ($interferingModuleNamesList in $interferingModuleNamesLists) {
                # Get required but interfering modules
                $requiredInterferingModuleNames = (Compare-Object -ReferenceObject $interferingModuleNamesList -DifferenceObject $requiredModules.Name -ExcludeDifferent -IncludeEqual).InputObject

                if ($requiredInterferingModuleNames.Count -eq 1) {
                    $interferingModuleNames += $interferingModuleNamesList
                } elseif ($requiredInterferingModuleNames.Count -gt 1) {
                    Write-Host -ForegroundColor "Red" "Script is not executable because interfering modules are required ($($requiredInterferingModuleNames -join ", "))."
                    exit
                }
            }

            if ($dependingModuleNames) {
                $installedDependingModules = @()
                foreach ($dependingModuleName in $dependingModuleNames) {
                    $requiredDependingModules = $requiredModules | Where-Object { $_.Name -like "$($dependingModuleName)*" }
                    if ($requiredDependingModules) {
                        if (($requiredDependingModules.RequiredVersion | Select-Object -Unique).Count -gt 1) {
                            Write-Error "Required '$($dependingModuleName)' modules have different versions."
                        } else {
                            $requiredDependingModuleVersion = $requiredDependingModules[0].RequiredVersion

                            $installedDependingModulesTemp = @()
                            $installedDependingModulesTemp += Get-Module -ListAvailable -Name "$($dependingModuleName)*" | Where-Object { $_.Version -ne $requiredDependingModuleVersion }

                            $installedDependingModules += $installedDependingModulesTemp | Where-Object { $_.Name -ne "Microsoft.Graph.Authentication" }
                            $installedDependingModules += $installedDependingModulesTemp | Where-Object { $_.Name -eq "Microsoft.Graph.Authentication" }
                        }
                    }
                }

                if ($installedDependingModules) {
                    Write-Host -ForegroundColor "DarkGray" -Object "Clean up depending modules ..."
                    foreach ($installedDependingModule in $installedDependingModules) {
                        Write-Host -ForegroundColor "Magenta" -Object " Module '$($installedDependingModule.Name)' ($($installedDependingModule.Version)) will be uninstalled ..."
                        Uninstall-Module -Name $installedDependingModule.Name -Force | Out-Null
                    }
                }
            }

            if ($interferingModuleNames) {
                Write-Host -ForegroundColor "DarkGray" -Object "Clean up interfering modules ..."
                foreach ($interferingModuleName in $interferingModuleNames) {
                    if ($requiredModules.Name -notContains $interferingModuleName) {
                        $installedInterferingModule = $null
                        $installedInterferingModule = Get-Module -ListAvailable -Name $interferingModuleName
                        if ($installedInterferingModule) {
                            Write-Host -ForegroundColor "Magenta" -Object " Interfering module '$($installedInterferingModule.Name)' ($($installedInterferingModule.Version)) will be uninstalled ..."
                            Uninstall-Module -Name $interferingModuleName -Force | Out-Null
                        }
                    }
                }
            }

            foreach ($requiredModule in $requiredModules) {
                $requiredModuleVersion = $requiredModule.RequiredVersion -join "."
                Write-Host -ForegroundColor "DarkGray" -Object "Module '$($requiredModule.Name)' ($requiredModuleVersion) is required ..."

                $installedModule = Get-Module -ListAvailable -Name $requiredModule.Name
                if ($installedModule) {
                    $installedModuleVersion = $installedModule.Version -join "."
                    if ($requiredModuleVersion -eq $installedModuleVersion) {
                        Write-Host -ForegroundColor "Green" -Object " Module '$($installedModule.Name)' ($installedModuleVersion) is already installed"
                    } else {
                        Write-Host -ForegroundColor "Magenta" -Object " Module '$($installedModule.Name)' ($installedModuleVersion) will be uninstalled ..."
                        Uninstall-Module -Name $installedModule.Name -Force | Out-Null

                        Write-Host -ForegroundColor "Cyan" -Object " Module '$($requiredModule.Name)' ($requiredModuleVersion) will be installed ..."
                        Install-Module -Name $requiredModule.Name -Scope AllUsers -Force -RequiredVersion $requiredModuleVersion | Out-Null
                    }
                } else {
                    Write-Host -ForegroundColor "Cyan" -Object " Module '$($requiredModule.Name)' ($requiredModuleVersion) is not present and will be installed ..."
                    Install-Module -Name $requiredModule.Name -Scope AllUsers -Force -RequiredVersion $requiredModuleVersion | Out-Null
                }
            }
        } else {
            Write-Host -ForegroundColor "DarkGray" -Object "No modules required"
        }
    } finally {
        # Reset ErrorActionPreference
        if (!$inputErrorAction) {
            $ErrorActionPreference = $activeErrorActionPreference
        }
    }
}