Public/Install-Dependency.ps1


Function Install-Dependency {

<#
.SYNOPSIS
    Downloads and installs the latest versions of some modules and packages (saved in current folder or machine folder) from the PowerShell Gallery and NuGet.
.DESCRIPTION
    Function to install dependencies from the PowerShell Gallery and NuGet. It is possible to install modules and packages.
    The packages can be installed from the PowerShell Gallery and packages from a NuGet repository.
    Packages can defined as a raw string array or as a pscustomobject with a specific version number.
 
    Uses Get-PSEnvironment (from the ImportDependency module) to discover already-installed modules and
    local/global NuGet packages, so it only installs or updates what is actually missing or outdated.
 
    Please make sure to have the Modules WriteLog and ImportDependency, and PowerShellGet (>= 2.2.4), installed.
 
.EXAMPLE
    Install-Dependency -Module "WriteLog" -LocalPackage "SQLitePCLRaw.core", "Npgsql" -Verbose
.EXAMPLE
    $packages = [Array]@(
        [PSCustomObject]@{
            name="Npgsql"
            version = "4.1.12"
            includeDependencies = $true
        }
    )
    Install-Dependency -Module "WriteLog" -LocalPackage $packages -Verbose
 
.PARAMETER Module
    Array of modules to install on local machine via PowerShellGallery.
.PARAMETER GlobalPackage
    Array of NuGet packages to install on local machine.
.PARAMETER LocalPackage
    Array of NuGet packages to install in a subfolder of the current folder. Can be changed with parameter LocalPackageFolder.
.PARAMETER LocalPackageFolder
    Folder name of the local package folder. Default is "lib".
.PARAMETER ExcludeDependencies
    By default, this script is installing dependencies for every nuget package. This can be deactivated with this switch.
.PARAMETER KeepPackage
    By default, the raw .nupkg files left behind by Install-Package -Destination inside LocalPackageFolder are
    removed after a successful install (matching Install-NuGetPackage's default). The .nuspec is extracted out
    of each .nupkg first, so the package stays discoverable by Get-LocalPackage/Import-Dependency. Set this
    switch to keep the .nupkg files instead.
.PARAMETER SuppressWarnings
    Flag to log warnings, but not redirect to the host.
.PARAMETER KeepLogfile
    Flag to keep an existing logfile rather than creating a new one.
.NOTES
    Created by : gitfvb
.LINK
    Project Site: https://github.com/Apteco/Install-Dependencies/tree/main/Install-Dependencies
#>



    [CmdletBinding()]
    Param(

         [Parameter(Mandatory=$false)]
         [String[]]$Module = [Array]@()

        ,[Parameter(Mandatory=$false)]
         [Object[]]$GlobalPackage = [Array]@()   # String, or PSCustomObject with name/version -- [String[]] would coerce PSCustomObjects to strings before this even runs

        ,[Parameter(Mandatory=$false)]
         [Object[]]$LocalPackage = [Array]@()    # String, or PSCustomObject with name/version -- [String[]] would coerce PSCustomObjects to strings before this even runs

        ,[Parameter(Mandatory=$false)]
         [String]$LocalPackageFolder = "lib"

        ,[Parameter(Mandatory=$false)]
         [Switch]$ExcludeDependencies = $false

        ,[Parameter(Mandatory=$false)]
         [Switch]$KeepPackage = $false        # Keep the raw .nupkg files in LocalPackageFolder after install (default: remove them, like Install-NuGetPackage)

        ,[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()]
         [Switch]$SuppressWarnings = $false           # Flag to log warnings, but not put redirect to the host

        ,[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()]
         [Switch]$KeepLogfile = $false           # Flag to keep existing logfile

    )


    Begin {

        # Set explicitly verbose output and remember it
        If ( $SuppressWarnings -ne $true -and $PSBoundParameters["Verbose"].IsPresent -eq $true -and $PSBoundParameters["Verbose"] -eq $True) {
            $originalVerbosity = $VerbosePreference
            $VerbosePreference = 'Continue'
        }

        Write-Verbose "Proceeding with start settings"


        #-----------------------------------------------
        # START
        #-----------------------------------------------

        $processStart = [datetime]::now
        $getLogfile = Get-Logfile

        If ( $KeepLogfile -eq $false -and $null -ne $getLogfile ) {
            $currentLogfile = Get-Logfile
            $logfile = ".\dependencies_install.log"
            $logfileAbsolute = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($logfile)
            Set-Logfile -Path $logfileAbsolute
            Write-Log -message "----------------------------------------------------" -Severity VERBOSE
            Write-Log -Message "Changed logfile from '$( $currentLogfile )' to '$( $logfileAbsolute )'"
        } else {
            $logfile = ".\dependencies_install.log"
            Set-Logfile -Path $logfile
            Write-Log -message "----------------------------------------------------" -Severity VERBOSE
        }

        # Remember the current processID
        $processId = Get-ProcessId

        $writeToHost = $true
        If ( $SuppressWarnings -eq $true ) {
            $writeToHost = $false
        }


        #-----------------------------------------------
        # DOING SOME CHECKS
        #-----------------------------------------------

        # Check if this is Pwsh Core. Also gathers already-installed modules and
        # local/global NuGet packages (for the given folder) so the checks below
        # don't need to re-query PowerShellGet/PackageManagement themselves.
        $psEnv = Get-PSEnvironment -LocalPackageFolder $LocalPackageFolder
        $isCore = $psEnv.IsCore
        Write-Log -Message "Using PowerShell version $( $psEnv.PSVersion ) and $( $psEnv.PSEdition ) edition"

        # Operating system
        $os = $psEnv.OS
        Write-Log -Message "Using OS: $( $os )"
        Write-Log -Message "Using architecture: $( $psEnv.Architecture )"

        # Check elevation
        if ($os -eq "Windows") {
            Write-Log -Message "User: $( $psEnv.ExecutingUser )"
            Write-Log -Message "Elevated: $( $psEnv.IsElevated )"
        } else {
            Write-Log -Message "No user and elevation check due to OS"
        }

        # Check execution policy
        Write-Log -Message "Your execution policy is currently: $( $psEnv.ExecutionPolicy.Process )" -Severity VERBOSE

        # Check if elevated rights are needed
        If ( $GlobalPackage.Count -gt 0 -and $psEnv.IsElevated -eq $false) {
            throw "To install global packages, you need elevated rights, so please restart PowerShell with Administrator privileges!"
        }


        #-----------------------------------------------
        # NUGET SETTINGS
        #-----------------------------------------------

        $packageSourceName = "NuGet v2"
        $packageSourceLocation = "https://www.nuget.org/api/v2"
        $packageSourceProviderName = "NuGet"


        #-----------------------------------------------
        # POWERSHELL GALLERY SETTINGS
        #-----------------------------------------------

        $powerShellSourceName = "PSGallery"
        $powerShellSourceLocation = "https://www.powershellgallery.com/api/v2"
        $powerShellSourceProviderName = "PowerShellGet"

        If ( $psEnv.IsElevated -eq $true ) {
            $psScope = "AllUsers"
        } else {
            $psScope = "CurrentUser"
        }

        Write-Log -Message "Using installation scope: $( $psScope )" -Severity VERBOSE

        # Initialise counters (used across Process and reported in End)
        $Script:installCount_m = 0
        $Script:installCount_l = 0
        $Script:installCount_g = 0

    }


    Process {

        #-----------------------------------------------
        # CHECK POWERSHELL GALLERY REPOSITORY
        #-----------------------------------------------

        # TODO Implement version checks with [System.Version]::Parse("x.y.z")
        If ( $Module.Count -gt 0 ) {
            $powershellRepo = @( Get-PackageSource -ProviderName $powerShellSourceProviderName )
            If ( $powershellRepo.Count -eq 0 ) {
                Write-Log "No module repository found! Please make sure to add a repository to your machine!" -Severity WARNING
            }
        }

        # Install newer PackageManagement if needed (already known from Get-PSEnvironment, no extra lookup needed).
        # -ProviderName disambiguates against NuGet.org, which coincidentally also hosts a package
        # named "PackageManagement" -- without it, Install-Package errors with "multiple packages matched".
        If ( $psEnv.PackageManagement -eq "1.0.0.1" -or [String]::IsNullOrEmpty( $psEnv.PackageManagement ) ) {
            Write-Log "PackageManagement is outdated with v$( $psEnv.PackageManagement ). This is updating it now." -Severity WARNING
            try {
                Install-Package -Name PackageManagement -ProviderName $powerShellSourceProviderName -Force -AllowClobber | Out-Null
                # The installed binaries won't be used until re-imported: 'Import-Module -Force' merely stacks
                # the new version alongside the old one still loaded in this process, and cmdlets keep resolving
                # to the old module. Removing first, then a bare re-import (auto-picks the newest available
                # version), is what actually swaps the active binaries in-session.
                Remove-Module -Name PackageManagement -Force -ErrorAction SilentlyContinue
                Import-Module -Name PackageManagement -ErrorAction Stop
                Write-Log "PackageManagement re-imported as v$( (Get-Module PackageManagement).Version )" -Severity VERBOSE
            } catch {
                Write-Log "Could not update PackageManagement: $( $_.Exception.Message )" -Severity WARNING
            }
        }

        # Install newer PowerShellGet if needed (already known from Get-PSEnvironment, no extra lookup needed)
        If ( $psEnv.PowerShellGet -eq "1.0.0.1" -or [String]::IsNullOrEmpty( $psEnv.PowerShellGet ) ) {
            Write-Log "PowerShellGet is outdated with v$( $psEnv.PowerShellGet ). This is updating it now." -Severity WARNING
            try {
                Install-Package -Name PowerShellGet -ProviderName $powerShellSourceProviderName -Force -AllowClobber | Out-Null
                # Same in-session reload issue as PackageManagement above.
                Remove-Module -Name PowerShellGet -Force -ErrorAction SilentlyContinue
                Import-Module -Name PowerShellGet -ErrorAction Stop
                Write-Log "PowerShellGet re-imported as v$( (Get-Module PowerShellGet).Version )" -Severity VERBOSE
            } catch {
                Write-Log "Could not update PowerShellGet: $( $_.Exception.Message )" -Severity WARNING
            }
        }

        If ( $Module.Count -gt 0 ) {

            try {

                # Get PowerShellGet sources
                $powershellRepo = @( Get-PackageSource -ProviderName $powerShellSourceProviderName )

                # See if PSRepo needs to get registered
                If ( $powershellRepo.count -ge 1 ) {
                    Write-Log -Message "You have at minimum 1 $( $powerShellSourceProviderName ) repository. Good!"  -Severity VERBOSE
                } elseif ( $powershellRepo.count -eq 0 ) {
                    Write-Log -Message "You don't have $( $powerShellSourceProviderName ) as a module source, do you want to register it now?" -Severity WARNING
                    $registerPsRepoDecision = $Host.UI.PromptForChoice("", "Register $( $powerShellSourceProviderName ) as repository?", @('&Yes'; '&No'), 1)
                    If ( $registerPsRepoDecision -eq "0" ) {

                        Register-PSRepository -Name $powerShellSourceName -SourceLocation $powerShellSourceLocation | Out-Null

                        # Load sources again
                        $powershellRepo = @( Get-PSRepository -ProviderName $powerShellSourceProviderName )

                    } else {
                        Write-Log "No package repository found! Please make sure to add a PowerShellGet repository to your machine!" -Severity ERROR
                        exit 0
                    }
                }

                # Choose repository
                If ( $powershellRepo.count -gt 1 ) {

                    $psGetSources = $powershellRepo.Name
                    $psGetSourceChoice = Request-Choice -title "Module Source" -message "Which $( $powerShellSourceProviderName ) repository do you want to use?" -choices $psGetSources
                    $psGetSource = $psGetSources[$psGetSourceChoice -1]

                } elseif ( $powershellRepo.count -eq 1 ) {

                    $psGetSource = $powershellRepo[0]

                } else {

                    Write-Log -Message "There is no $( $powerShellSourceProviderName ) repository available"  -Severity WARNING
                    Exit 0

                }

                # Do you want to trust that source?
                If ( $psGetSource.IsTrusted -eq $false ) {
                    Write-Log -Message "Your source is not trusted. Do you want to trust it now?" -Severity WARNING
                    $trustChoice = Request-Choice -title "Trust module Source" -message "Do you want to trust $( $psGetSource.Name )?" -choices @("Yes", "No")
                    If ( $trustChoice -eq 1 ) {
                        Set-PSRepository -Name $psGetSource.Name -InstallationPolicy Trusted | Out-Null
                    }
                }

            } catch {

                Write-Log -Message "There is a problem with the repository check!" -Severity WARNING

            }

        }


        #-----------------------------------------------
        # CHECK MODULES DEPENDENCIES FOR INSTALLATION AND UPDATE
        #-----------------------------------------------

        If ( $Module.count -gt 0 ) {

            try {

                Write-Log "Checking Module dependencies" -Severity VERBOSE

                # Already known from Get-PSEnvironment (scanned once in Begin), so no
                # per-module Get-InstalledModule round-trip is needed here anymore.
                $installedModules = @( $psEnv.InstalledModules | Where-Object { $null -ne $_ } )

                $Module | Where-Object { $_ -notin @("PowerShellGet","PackageManagement") } | ForEach-Object {

                    $psModule = $_

                    Write-Log "Checking module: $( $psModule )" -Severity VERBOSE

                    If ( $ExcludeDependencies -eq $true ) {
                        $psModuleDependencies = Find-Module -Name $psModule
                    } else {
                        $psModuleDependencies = Find-Module -Name $psModule -IncludeDependencies
                    }

                    $psModuleDependencies | ForEach-Object {

                        $mod = $_
                        $installedMatches = @( $installedModules | Where-Object { $_.Name -eq $mod.Name } )

                        If ( $installedMatches.Count -gt 0 ) {

                            Write-Log -Message "Module $( $mod.Name ) is already installed" -Severity VERBOSE

                            # Multiple entries can exist per module (e.g. WindowsPowerShell vs PSCore paths), take the newest
                            $alreadyInstalledModule = $installedMatches | Sort-Object { [version]( $_.Version -replace '[^0-9.]', '0' ) } -Descending | Select-Object -First 1

                            If ( [version]( $mod.Version.ToString() -replace '[^0-9.]', '0' ) -gt [version]( $alreadyInstalledModule.Version -replace '[^0-9.]', '0' ) ) {
                                Write-Log -Message "Module $( $mod.Name ) is installed with an older version $( $alreadyInstalledModule.Version ) than the available version $( $mod.Version )" -Severity VERBOSE
                                Update-Module -Name $mod.Name | Out-Null
                                $Script:installCount_m += 1
                            } else {
                                Write-Log -Message "No need to update $( $mod.Name )" -Severity VERBOSE
                            }
                        } else {
                            Write-Log -Message "Installing Module $( $mod.Name )" -Severity VERBOSE
                            Install-Module -Name $mod.Name -Scope $psScope -AllowClobber | Out-Null
                            $Script:installCount_m += 1
                        }

                    }

                }

            } catch {

                Write-Log -Message "Cannot install modules!" -Severity WARNING

            }

        } else {

            Write-Log "There is no module to install" -Severity VERBOSE

        }


        #-----------------------------------------------
        # CHECK PACKAGES NUGET REPOSITORY
        #-----------------------------------------------

        If ( $GlobalPackage.Count -gt 0 -or $LocalPackage.Count -gt 0 ) {

            try {

                # Get NuGet sources
                $sources = @( Get-PackageSource -ProviderName $packageSourceProviderName )

                # See if Nuget needs to get registered
                If ( $sources.count -ge 1 ) {
                    Write-Log -Message "You have at minimum 1 $( $packageSourceProviderName ) repository. Good!" -Severity VERBOSE
                } elseif ( $sources.count -eq 0 ) {
                    Write-Log -Message "You don't have $( $packageSourceProviderName ) as a PackageSource, do you want to register it now?" -Severity WARNING
                    $registerNugetDecision = $Host.UI.PromptForChoice("", "Register $( $packageSourceProviderName ) as repository?", @('&Yes'; '&No'), 1)
                    If ( $registerNugetDecision -eq "0" ) {

                        Register-PackageSource -Name $packageSourceName -Location $packageSourceLocation -ProviderName $packageSourceProviderName | Out-Null

                        # Load sources again
                        $sources = @( Get-PackageSource -ProviderName $packageSourceProviderName )

                    } else {
                        Write-Log "No package repository found! Please make sure to add a NuGet repository to your machine!" -Severity ERROR
                        exit 0
                    }
                }

                # Choose repository
                If ( $sources.count -gt 1 ) {

                    $packageSources = $sources.Name
                    $packageSourceChoice = Request-Choice -title "PackageSource" -message "Which $( $packageSourceProviderName ) repository do you want to use?" -choices $packageSources
                    $packageSource = $sources[$packageSourceChoice -1]

                } elseif ( $sources.count -eq 1 ) {

                    $packageSource = $sources[0]

                } else {

                    Write-Log -Message "There is no $( $packageSourceProviderName ) repository available" -Severity WARNING
                    Exit 0

                }

                # Do you want to trust that source?
                If ( $packageSource.IsTrusted -eq $false ) {
                    Write-Log -Message "Your source is not trusted. Do you want to trust it now?" -Severity WARNING
                    $trustChoice = Request-Choice -title "Trust Package Source" -message "Do you want to trust $( $packageSource.Name )?" -choices @("Yes", "No")
                    If ( $trustChoice -eq 1 ) {
                        Set-PackageSource -Name $packageSource.Name -Trusted | Out-Null
                    }
                }

            } catch {

                Write-Log -Message "There is a problem with the repository" -Severity WARNING

            }

        }


        #-----------------------------------------------
        # CHECK LOCAL AND GLOBAL PACKAGES FOR INSTALLATION AND UPDATE
        #-----------------------------------------------

        If ( $LocalPackage.count -gt 0 -or $GlobalPackage.Count -gt 0) {

            try {

                Write-Log "Check lib folder" -Severity VERBOSE

                If ( (Test-Path -Path $LocalPackageFolder) -eq $false ) {
                    New-Item -Path $LocalPackageFolder -ItemType Directory
                }

                Write-Log "Checking package dependencies with $( $packageSource.Name )" -Severity VERBOSE

                # Already known from Get-PSEnvironment (scanned once in Begin for $LocalPackageFolder),
                # so no separate Get-Package round-trip is needed here anymore.
                $installedLocalPackages  = @( $psEnv.InstalledLocalPackages  | Where-Object { $null -ne $_ } )
                $installedGlobalPackages = @( $psEnv.InstalledGlobalPackages | Where-Object { $null -ne $_ } )
                $packagesToInstall = [System.Collections.ArrayList]@()
                @( $LocalPackage + $GlobalPackage ) | ForEach-Object {

                    $psPackage = $_
                    $globalFlag = $false
                    $pkg = [System.Collections.ArrayList]@()
                    If ( $GlobalPackage -contains $psPackage ) {
                        $globalFlag = $true
                    }

                    # An explicit pinned version (e.g. to keep an older, edition-specific build like
                    # DuckDB.NET 1.4.4 installed alongside a newer 1.5.0 for the same package Id) means
                    # this exact version is the thing that must exist on disk -- not "any version at
                    # least this new", which is the right semantics for an unpinned "give me latest" request
                    $isPinned = ( ($psPackage.gettype()).Name -eq "PsCustomObject" ) -and ( $null -ne $psPackage.version )

                    Write-Log "Checking package: $( $psPackage )" -severity VERBOSE

                    If ( ($psPackage.gettype()).Name -eq "PsCustomObject" ) {
                        If ( $null -eq $psPackage.version ) {
                            Write-Verbose "Looking for $( $psPackage.name ) without specific version."
                            If ( $ExcludeDependencies -eq $true ) {
                                [void]@( Find-Package $psPackage.name -Source $packageSource.Name -ErrorAction Continue ).foreach({$pkg.add($_)})
                            } else {
                                [void]@( Find-Package $psPackage.name -IncludeDependencies -Source $packageSource.Name -ErrorAction Continue ).foreach({$pkg.add($_)})
                            }
                        } else {
                            Write-Verbose "Looking for $( $psPackage.name ) with version $( $psPackage.version )"
                            If ( $ExcludeDependencies -eq $true ) {
                                [void]@( Find-Package $psPackage.name -Source $packageSource.Name -ErrorAction Continue -RequiredVersion $psPackage.version ).foreach({$pkg.add($_)})
                            } else {
                                [void]@( Find-Package $psPackage.name -IncludeDependencies -Source $packageSource.Name -ErrorAction Continue -RequiredVersion $psPackage.version ).foreach({$pkg.add($_)})
                            }
                        }
                    } else {
                        Write-Verbose "Looking for $( $psPackage ) without specific version"
                        If ( $ExcludeDependencies -eq $true ) {
                            [void]@( Find-Package $psPackage -Source $packageSource.Name -ErrorAction Continue ).foreach({$pkg.add($_)})
                        } else {
                            [void]@( Find-Package $psPackage -IncludeDependencies -Source $packageSource.Name -ErrorAction Continue ).foreach({$pkg.add($_)})
                        }
                    }

                    $installedForScope = If ( $globalFlag -eq $true ) { $installedGlobalPackages } else { $installedLocalPackages }

                    $pkg | ForEach-Object {

                        $p = $_
                        $installedPackageMatches = @( $installedForScope | Where-Object { $_.Id -eq $p.Name } )

                        If ( $installedPackageMatches.Count -gt 0 ) {

                            If ( $isPinned -eq $true ) {

                                # Exact version was explicitly requested -- only this exact version on disk
                                # satisfies it. A newer sibling version installed for another package Id
                                # purpose (e.g. a different PS edition) must not shadow this pinned install
                                $exactMatch = @( $installedPackageMatches | Where-Object { $_.Version -eq $p.Version.ToString() } )
                                If ( $exactMatch.Count -gt 0 ) {
                                    Write-Log -Message "Package $( $p.Name ) $( $p.Version ) is already installed ($( If ( $globalFlag ) { 'global' } else { 'local' } )), skipping" -Severity VERBOSE
                                    return
                                } else {
                                    Write-Log -Message "Package $( $p.Name ) $( $p.Version ) is pinned but not yet installed (other version(s) present: $( ( $installedPackageMatches.Version -join ', ' ) )), installing" -Severity VERBOSE
                                }

                            } else {

                                # Multiple copies can exist on disk (nuspec/zip reads), take the newest
                                $alreadyInstalledPackage = $installedPackageMatches | Sort-Object { [version]( $_.Version -replace '[^0-9.]', '0' ) } -Descending | Select-Object -First 1

                                If ( [version]( $alreadyInstalledPackage.Version -replace '[^0-9.]', '0' ) -ge [version]( $p.Version.ToString() -replace '[^0-9.]', '0' ) ) {
                                    Write-Log -Message "Package $( $p.Name ) is already installed with version $( $alreadyInstalledPackage.Version ) ($( If ( $globalFlag ) { 'global' } else { 'local' } )), skipping" -Severity VERBOSE
                                    return
                                } else {
                                    Write-Log -Message "Package $( $p.Name ) is installed with an older version $( $alreadyInstalledPackage.Version ) than the available version $( $p.Version )" -Severity VERBOSE
                                }

                            }

                        }

                        $pd = [PSCustomObject]@{
                            "GlobalFlag" = $globalFlag
                            "Package"    = $p
                            "Name"       = $p.Name
                            "Version"    = $p.Version
                        }
                        [void]$packagesToInstall.Add($pd)
                    }

                }

                Write-Log -Message "Done with searching for $( $packagesToInstall.Count ) packages"

                # @() forces this to always be an array, even with 0 or 1 matches -- without it, a
                # single match collapses to a scalar object. Windows PowerShell 5.1 (Desktop, unlike
                # PS 6+) has no .Count on scalars, so $pack.Count below is $null, and dividing by it
                # in Write-Progress throws "Attempted to divide by zero", aborting the whole install
                $pack = @( $packagesToInstall | Where-Object { $_.Package.Summary -notlike "*not reference directly*" -and $_.Package.Name -notlike "Xamarin.*"} | Where-Object { $_.Package.Source -eq $packageSource.Name } | Sort-Object Name, Version -Unique -Descending )
                Write-Log -Message "This is likely to install $( $pack.Count ) packages"

                $i = 0
                $pack | ForEach-Object {

                    $p = $_

                    If ( $p.GlobalFlag -eq $true ) {
                        Write-Log -message "Installing $( $p.Package.Name ) with version $( $p.Package.version ) from $( $p.Package.Source ) globally"
                        Install-Package -Name $p.Name -Scope $psScope -Source $packageSource.Name -RequiredVersion $p.Version -SkipDependencies -Force | Out-Null
                        $Script:installCount_g += 1
                    } else {
                        Write-Log -message "Installing $( $p.Name ) with version $( $p.version ) from $( $p.Package.Source ) locally"
                        Install-Package -Name $p.Name -Scope $psScope -Source $packageSource.Name -RequiredVersion $p.Version -SkipDependencies -Force -Destination $LocalPackageFolder | Out-Null
                        $Script:installCount_l += 1
                    }

                    Write-Progress -Activity "Package installation in progress" -Status "$( [math]::Round($i/$pack.Count*100) )% Complete:" -PercentComplete ([math]::Round($i/$pack.Count*100))
                    $i += 1

                }

                # Install-Package -Destination leaves the raw .nupkg file as a sibling of the
                # extracted lib/ref/runtimes folders. Only touches LocalPackageFolder, never the
                # global/machine package cache, which is shared state we should not clean up here.
                If ( $KeepPackage -eq $false -and $Script:installCount_l -gt 0 ) {
                    $nupkgFiles = @( Get-ChildItem -Path $LocalPackageFolder -Filter "*.nupkg" -Recurse -File -ErrorAction SilentlyContinue )
                    If ( $nupkgFiles.Count -gt 0 ) {
                        Write-Log -Message "Removing $( $nupkgFiles.Count ) .nupkg file(s) from '$( $LocalPackageFolder )' (use -KeepPackage to keep them)" -Severity VERBOSE
                        $nupkgFiles | ForEach-Object {

                            $nupkgFile = $_
                            $pkgFolder = $nupkgFile.DirectoryName

                            # Extract just the tiny .nuspec first so Get-LocalPackage (ImportDependency) can
                            # still discover this package via its fast nuspec path once the .nupkg is gone --
                            # without it, a package folder with only lib/ref/runtimes has no metadata source
                            # left at all and Import-Dependency silently stops finding it.
                            #
                            # Retried: Install-Package -Destination can still be holding a lock on the
                            # freshly written .nupkg for a moment (observed on Windows PowerShell 5.1's
                            # legacy PackageManagement NuGet provider), so an immediate open/delete can fail
                            # with a sharing violation. Never let that abort the rest of the install --
                            # worst case the .nupkg is just left behind for this one package.
                            $attempt = 0
                            $cleanedUp = $false
                            do {
                                $attempt += 1
                                try {
                                    $zip = [System.IO.Compression.ZipFile]::OpenRead($nupkgFile.FullName)
                                    try {
                                        $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1
                                        If ( $null -ne $nuspecEntry ) {
                                            $nuspecPath = Join-Path $pkgFolder $nuspecEntry.Name
                                            If ( (Test-Path -Path $nuspecPath) -eq $false ) {
                                                [System.IO.Compression.ZipFileExtensions]::ExtractToFile($nuspecEntry, $nuspecPath) | Out-Null
                                            }
                                        }
                                    } finally {
                                        $zip.Dispose()
                                    }

                                    Remove-Item -Path $nupkgFile.FullName -Force
                                    $cleanedUp = $true
                                } catch {
                                    If ( $attempt -ge 3 ) {
                                        Write-Log -Message "Could not remove '$( $nupkgFile.FullName )' after $( $attempt ) attempt(s): $( $_.Exception.Message )" -Severity WARNING
                                        $cleanedUp = $true
                                    } else {
                                        Start-Sleep -Milliseconds 300
                                    }
                                }
                            } while ( $cleanedUp -eq $false )

                        }
                    }
                }

            } catch {

                Write-Log -Message "Cannot install local packages! $( $_.Exception.Message )" -Severity WARNING

            }

        } else {

            Write-Log "There is no package to install" -Severity VERBOSE

        }

        # Reset the process ID if another module overrode it
        Set-ProcessId -Id $processId

    }


    End {

        #-----------------------------------------------
        # STATUS
        #-----------------------------------------------

        Write-Log -Message "STATUS:" -Severity INFO
        Write-Log -Message " $( $Script:installCount_l ) local packages installed into '$( $LocalPackageFolder )'" -Severity INFO
        Write-Log -Message " $( $Script:installCount_g ) global packages installed" -Severity INFO
        Write-Log -Message " $( $Script:installCount_m ) modules installed with scope '$( $psScope )'" -Severity INFO


        #-----------------------------------------------
        # FINISHING
        #-----------------------------------------------

        $processEnd = [datetime]::now
        $processDuration = New-TimeSpan -Start $processStart -End $processEnd
        Write-Log -Message "Done! Needed $( [int]$processDuration.TotalSeconds ) seconds in total" -Severity INFO

        Write-Log -Message "Logfile override: $( Get-LogfileOverride )"

        If ( $KeepLogfile -eq $false -and $null -ne $getLogfile -and '' -ne $getLogfile ) {
            Write-Log -Message "Changing logfile back to '$( $currentLogfile )'"
            Set-Logfile -Path $currentLogfile
        }

        # Set explicitly verbose output back
        If ( $SuppressWarnings -ne $true -and $PSBoundParameters["Verbose"].IsPresent -eq $true -and $PSBoundParameters["Verbose"] -eq $True) {
            $VerbosePreference = $originalVerbosity
        }

    }

}