Pentia.Publish-NuGetPackage.psm1

<#
.SYNOPSIS
Downloads the latest nuget.exe from the web and saves it to "<current directory>\.pentia\nuget.exe".
#>

function Install-NuGetExe {
    [CmdletBinding()]
    param ()

    if (Test-NuGetInstall) {
        Write-Verbose "nuget.exe is already installed."
        return
    }

    $saveToPath = Get-NuGetPath
    Write-Verbose "Downloading nuget.exe to '$saveToPath'"
    $sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
    $directoryPath = Split-Path -Path $saveToPath -Parent
    New-Item -ItemType Directory -Force -Path $directoryPath | Out-Null
    Invoke-WebRequest $sourceNugetExe -OutFile $saveToPath
}

function Test-NuGetInstall {
    Get-NuGetPath | Test-Path
}

function Get-NuGetPath {
    $localNuGetExePath = [System.IO.Path]::Combine("$PWD", ".pentia", "nuget.exe")
    $globalNuGetExePath = Get-GlobalNuGetPath
    if ([string]::IsNullOrWhiteSpace($globalNuGetExePath)) {
        return $localNuGetExePath
    }
    $globalNuGetExePath
}

function Get-GlobalNuGetPath {
    $latestGlobalNuGetExePath = Get-Command "nuget.exe" -ErrorAction SilentlyContinue | Sort-Object -Property "Version" -Descending | Select-Object -ExpandProperty "Source" -First 1
    $latestGlobalNuGetExePath
}

<#
.SYNOPSIS
A thin wrapper for "NuGet.exe restore [...]". See https://docs.microsoft.com/en-us/nuget/tools/cli-ref-restore.
 
.PARAMETER NuGetExePath
Path to NuGet.exe. Defaults to "<current directory>\.pentia\nuget.exe".
 
.PARAMETER SolutionDirectory
Specifies the solution folder. Not valid when restoring packages for a solution.
 
.PARAMETER OutputDirectory
Specifies the folder in which packages are installed. If no folder is specified, the current folder is used.
 
.PARAMETER ConfigFile
The NuGet configuration file to apply. If not specified, %AppData%\NuGet\NuGet.Config is used.
 
.PARAMETER NoCache
Prevents NuGet from using packages from local machine caches.
#>

function Restore-NuGetPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$NuGetExePath = (Get-NuGetPath),

        [Parameter(Mandatory = $false)]
        [string]$SolutionDirectory,

        [Parameter(Mandatory = $false)]
        [string]$OutputDirectory,

        [Parameter(Mandatory = $false)]
        [string]$ConfigFile,

        [switch]$NoCache
    )
    $builder = New-Object OptionBuilder
    $builder.Command = "restore"
    $builder.SolutionDirectory = $SolutionDirectory
    $builder.OutputDirectory = $OutputDirectory
    $builder.ConfigFile = $ConfigFile
    $builder.NoCache = $NoCache
    $options = $builder.Build()
    & "$NuGetExePath" $options
    if ($LASTEXITCODE -ne 0) {
        throw "NuGet command failed."
    }
}

<#
.SYNOPSIS
A thin wrapper for "NuGet.exe install [...]". See https://docs.microsoft.com/en-us/nuget/tools/cli-ref-install.
 
.PARAMETER NuGetExePath
Path to NuGet.exe. Defaults to "<current directory>\.pentia\nuget.exe".
 
.PARAMETER PackageId
The package to install. Can't be used with "PackageConfigFile".
 
.PARAMETER PackageVersion
The package version to install. Defaults to latest version.
 
.PARAMETER PackageConfigFile
The package.config file specifying which packages to install. Can't be used with "PackageId" and "PackageVersion".
 
.PARAMETER SolutionDirectory
Specifies the solution folder. Not valid when restoring packages for a solution.
 
.PARAMETER OutputDirectory
Specifies the folder in which packages are installed. If no folder is specified, the current folder is used.
 
.PARAMETER ConfigFile
The NuGet configuration file to apply. If not specified, %AppData%\NuGet\NuGet.Config is used.
 
.PARAMETER NoCache
Prevents NuGet from using packages from local machine caches.
#>

function Install-NuGetPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$NuGetExePath = (Get-NuGetPath),

        [Parameter(ParameterSetName = "InstallByPackageId", Mandatory = $true)]
        [string]$PackageId,

        [Parameter(ParameterSetName = "InstallByPackageId", Mandatory = $false)]
        [string]$PackageVersion,

        [Parameter(ParameterSetName = "InstallByPackageConfig", Mandatory = $true)]
        [string]$PackageConfigFile,

        [Parameter(Mandatory = $false)]
        [string]$SolutionDirectory,

        [Parameter(Mandatory = $false)]
        [string]$OutputDirectory,

        [Parameter(Mandatory = $false)]
        [string]$ConfigFile,

        [switch]$NoCache
    )
    $builder = New-Object InstallOptionBuilder
    $builder.Command = "install"
    $builder.PackageId = $PackageId
    $builder.PackageVersion = $PackageVersion
    $builder.PackageConfigFile = $PackageConfigFile
    $builder.SolutionDirectory = $SolutionDirectory
    $builder.OutputDirectory = $OutputDirectory
    $builder.ConfigFile = $ConfigFile
    $builder.NoCache = $NoCache
    $options = $builder.Build()
    & "$NuGetExePath" $options
    if ($LASTEXITCODE -ne 0) {
        throw "NuGet command failed."
    }
}

Class OptionBuilder {
    [string]$Command
    [string]$SolutionDirectory
    [string]$OutputDirectory
    [string]$ConfigFile
    [bool]$NoCache
    Hidden [System.Collections.ArrayList]$Options

    OptionBuilder () {
        $this.Options = @()
    }

    [System.Collections.ArrayList] Build() {
        $this.AddParameter($this.Command)
        $this.AddParameter("-SolutionDirectory", $this.SolutionDirectory)
        $this.AddParameter("-OutputDirectory", $this.OutputDirectory)
        $this.AddParameter("-ConfigFile", $this.ConfigFile)
        if ($this.NoCache) {
            $this.AddParameter("-NoCache")
        }
        $this.AddParameter("-NonInteractive")
        return $this.Options
    }

    Hidden [void] AddParameter([string]$ParameterName) {
        $this.Options.Add($ParameterName)
    }

    Hidden [void] AddParameter([string]$ParameterName, [string]$ParameterValue) {
        if (-not [string]::IsNullOrWhiteSpace($ParameterValue)) {
            $this.Options.Add($ParameterName)
            $this.Options.Add($ParameterValue)
        }
    }
}

Class InstallOptionBuilder : OptionBuilder {
    [string]$PackageId
    [string]$PackageVersion
    [string]$PackageConfigFile

    [System.Collections.ArrayList] Build() {
        ([OptionBuilder]$this).Build()
        $this.InsertParameter(1, $this.PackageId)
        $this.AddParameter("-Version", $this.PackageVersion)
        $this.InsertParameter(1, $this.PackageConfigFile)
        return $this.Options
    }

    Hidden [void] InsertParameter([int]$Index, [string]$ParameterValue) {
        if (-not [string]::IsNullOrWhiteSpace($ParameterValue)) {
            $this.Options.Insert($Index, $ParameterValue)
        }
    }
}

<#
.SYNOPSIS
Publishes the contents of a runtime dependency package to a website, using NuGet.
 
.DESCRIPTION
Publishes the contents of a runtime dependency package to a website, using NuGet.
 
Packages are expected to be NuGet-packages and contain any of the following folders:
 
- <package>/Webroot
- <package>/Data
 
All of the above are optional.
 
The following steps are performed during package publishing:
 
1. Check if the required package is cached locally.
1.1 If the package isn't found locally, it's installed from a registered package source, or from the $PackageSource parameter.
2. Copy the contents of the "<package>\Webroot"-folder to the "<WebrootOutputPath>".
3. Copy the contents of the "<package>\Data"-folder to the "<DataOutputPath>".
 
.PARAMETER PackageName
The name of the package to publish.
 
.PARAMETER PackageVersion
The exact version of the package to publish.
 
.PARAMETER PackageOutputPath
The location of the installed NuGet packages (e.g. "<solution root>/.pentia/runtime-dependencies/").
 
.PARAMETER WebrootOutputPath
The path where the contents of "<package>\Webroot" will be copied to.
 
.PARAMETER DataOutputPath
The path where the contents of "<package>\Data" will be copied to.
#>

function Publish-NuGetPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,

        [Parameter(Mandatory = $true)]
        [string]$PackageVersion,

        [Parameter(Mandatory = $true)]
        [string]$PackageOutputPath,

        [Parameter(Mandatory = $true)]
        [string]$WebrootOutputPath,

        [Parameter(Mandatory = $true)]
        [string]$DataOutputPath
    )
    $packagePath = [System.IO.Path]::Combine($PackageOutputPath, "$PackageName.$PackageVersion")
    $packageWebrootPath = [System.IO.Path]::Combine($PackagePath, "webroot")
    Copy-PackageFolder -SourceFriendlyName "webroot" -Source $packageWebrootPath -Target $WebrootOutputPath
    $packageDataPath = [System.IO.Path]::Combine($PackagePath, "data")
    Copy-PackageFolder -SourceFriendlyName "data" -Source $packageDataPath -Target $DataOutputPath
}

function Copy-PackageFolder {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$SourceFriendlyName,

        [Parameter(Mandatory = $true)]
        [string]$Source,

        [Parameter(Mandatory = $true)]
        [string]$Target
    )

    Write-Verbose "Checking if package has a $SourceFriendlyName folder '$Source'."
    if (Test-Path -Path $Source -PathType Container) {
        Write-Verbose "Copying $SourceFriendlyName files from '$Source' to '$Target'."
        Invoke-RoboCopy -Source $Source -Target $Target
        $global:LASTEXITCODE = Convert-RoboCopyExitCode -ExitCode $LASTEXITCODE
    }
    else {
        Write-Verbose "No $SourceFriendlyName folder found."
    }
}

function Invoke-RoboCopy {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Source,

        [Parameter(Mandatory = $true)]
        [string]$Target
    )
    $roboCopyExeFilePath = Get-Command -Name "Robocopy.exe"
    & $roboCopyExeFilePath "$Source" "$Target" *.* /E /R:0 /MT:64 /NFL /NP /NDL /NJH | Write-Verbose
}

<#
Converts the RoboCopy exit code (https://ss64.com/nt/robocopy-exit.html) to exit codes usable by PowerShell (i.e. 0 on success, non-zero on failure).
#>

function Convert-RoboCopyExitCode {
    [CmdletBinding()]
    [OutputType([int])]
    param (
        [Parameter(Mandatory = $true)]
        [int]$ExitCode
    )
    switch ($ExitCode) {
        0 {
            Write-Verbose "No errors occurred, and no copying was done. The source and destination directory trees are completely synchronized."
            return 0
        }
        1 {
            Write-Verbose "One or more files were copied successfully (that is, new files have arrived)."
            return 0
        }
        2 {
            Write-Verbose "Some extra files or directories were detected. No files were copied. Examine the output log for details."
            return 0
        }
        3 {
            Write-Verbose "Some files were copied. Additional files were present. No failure was encountered."
            return 0
        }
        4 {
            Write-Warning "Some mismatched files or directories were detected. Examine the output log. Housekeeping might be required."
            return 0
        }
        5 {
            Write-Verbose "Some files were copied. Some files were mismatched. No failure was encountered."
            return 0
        }
        6 {
            Write-Verbose "Additional files and mismatched files exist. No files were copied and no failures were encountered. This means that the files already exist in the destination directory."
            return 0
        }
        7 {
            Write-Verbose "Files were copied, a file mismatch was present, and additional files were present."
            return 0
        }
        default {
            Write-Error "Error during file copy. RoboCopy exit code '$ExitCode'. See https://ss64.com/nt/robocopy-exit.html for details."
            return $ExitCode
        }
    }
}

Export-ModuleMember -Function Install-NuGetExe, Restore-NuGetPackage, Install-NuGetPackage, Publish-NuGetPackage