PSBuilder.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'

##### BEGIN Exit-Powershell.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Exit-Powershell {
    param ([int]$ExitCode=0)

    exit $ExitCode
 }
##### END Exit-Powershell.ps1 #####


##### BEGIN Invoke-Builder.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-Builder
{
    [CmdletBinding(DefaultParameterSetName="Default")]
    param (
        [Parameter(Position=0, ValueFromRemainingArguments=$true)]
        [string[]]$Tasks,

        [Parameter(Mandatory=$false)]
        [hashtable]$Parameters = $null,

        [switch]$ExitOnError
    )

    $buildRoot = (Get-Location).Path
    $buildFile = "$PSScriptRoot/files/build.tasks.ps1"
    $buildScript = Get-Command -Name $buildFile

    $buildParameters = @{ "BuildRoot" = $buildRoot }
    (Get-ChildItem -Path "Env:").Where({ $_.Name -like "PSBuilder*" }).ForEach({ $buildParameters[$_.Name.Substring(9)] = $_.Value })
    if ($null -ne $Parameters) { $buildParameters += $Parameters }

    foreach ($parameter in @($buildParameters.Keys))
    {
        if (-not $buildScript.Parameters.ContainsKey($parameter))
        {
            throw "Unknown parameter: $parameter"
        }

        $buildParameterType = $buildScript.Parameters[$parameter].ParameterType
        $currentValue = $buildParameters[$parameter]
        if ($buildParameterType -eq [string[]] -and $currentValue -is [string])
        {
            $buildParameters[$parameter] = $currentValue -split ","
        }
        elseif ($buildParameterType -eq [securestring])
        {
            $value = [securestring]::new()
            $currentValue.ToCharArray().ForEach({ $value.AppendChar($_) })
            $buildParameters[$parameter] = $value
        }
        elseif ($buildParameterType -eq [bool])
        {
            $buildParameters[$parameter] = [Convert]::ToBoolean($buildParameters[$parameter])
        }
        elseif ($buildParameterType -eq [int])
        {
            $buildParameters[$parameter] = [Convert]::ToInt32($buildParameters[$parameter])
        }
        elseif ($buildParameterType -eq [datetime])
        {
            $buildParameters[$parameter] = [Convert]::ToDateTime($buildParameters[$parameter])
        }
    }

    try
    {
        $failed = $false
        Invoke-Build -Task $Tasks -File $BuildFile -Result "result" @buildParameters
    }
    catch
    {
        $failed = $true
        if (-not $ExitOnError) { throw }
    }


    if ($failed -or $result.Errors.Count -gt 0)
    {
        if ($ExitOnError)
        {
            Exit-Powershell -ExitCode 1
        }
        else
        {
            throw "Build Failed..."
        }
    }
}
##### END Invoke-Builder.ps1 #####


##### BEGIN Invoke-CompileModule.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-CompileModule
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Name,

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

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

    if (-not [IO.Path]::IsPathRooted($Source)) { $Source = Resolve-Path -Path $Source }
    if (-not [IO.Path]::IsPathRooted($Destination)) { $Destination = [IO.Path]::GetFullPath($Destination) }

    $buildFolders = ("Classes", "Private", "Public")
    $SourceFile = Join-Path -Path $Source -ChildPath "$Name.psm1"
    if (Test-Path -Path $SourceFile)
    {
        Copy-Item -Path $SourceFile -Destination $Destination

        foreach ($buildFolder in $buildFolders)
        {
            $path = Join-Path -Path $Source -ChildPath $buildFolder
            if (Test-Path -Path $path)
            {
                Copy-Item -Path $path -Destination $BuildOutput -Recurse -Container -Force
            }
        }
    }
    else
    {
        $publicFolder = Join-Path -Path $Source -ChildPath "Public"
        $publicFunctions = @(Get-ChildItem -Path $publicFolder -Filter "*.ps1" -Recurse).ForEach({ $_.BaseName })

        $builder = [System.Text.StringBuilder]::new()
        [void]$builder.AppendLine("Set-StrictMode -Version Latest")
        [void]$builder.AppendLine("`$ErrorActionPreference='Stop'")


        foreach ($buildFolder in $buildFolders)
        {
            $path = Join-Path -Path $Source -ChildPath $buildFolder
            if (-not (Test-Path -Path $path)) { continue }
            $files = Get-ChildItem -Path $path -Filter "*.ps1" -Recurse

            foreach ($file in $files)
            {
                $content = Get-Content -Path $file.FullName -Raw
                [void]$builder.AppendLine("")
                [void]$builder.AppendLine("##### BEGIN $($file.Name) #####")
                [void]$builder.AppendLine("#.ExternalHelp $Name-Help.xml")
                [void]$builder.AppendLine($content)
                [void]$builder.AppendLine("##### END $($file.Name) #####")
                [void]$builder.AppendLine("")
            }
        }

        if ($publicFunctions.Count -gt 0)
        {
            [void]$builder.AppendLine("Export-ModuleMember -Function @($($publicFunctions.ForEach({ "'$_'" }) -join ", "))")
        }

        Set-Content -Path $Destination -Value ($builder.ToString()) -Force
    }
}
##### END Invoke-CompileModule.ps1 #####


##### BEGIN Invoke-CreateModuleManifest.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-CreateModuleManifest
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Name,

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

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

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

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

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

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

        [string]$LicenseUri = $null,
        [string]$IconUri = $null,
        [string]$ProjectUri = $null,
        [string[]]$Tags = $null,
        [string]$Prerelease = $null
    )

    $module = Get-Module -Name $ModuleFilePath -ListAvailable
    $Exports = @{
        "Aliases" = @($module.ExportedAliases.Keys)
        "Cmdlets" = @($module.ExportedCmdlets.Keys)
        "Functions" = @($module.ExportedFunctions.Keys)
        "Variables" = @($module.ExportedVariables.Keys)
    }

    $ManifestArguments = @{
        "RootModule" = "$Name.psm1"
        "Guid" = $Guid
        "Author" = $Author
        "Description" = $Description
        "Copyright" = "(c) $((Get-Date).Year) $Author. All rights reserved."
        "AliasesToExport" = $Exports.Aliases
        "CmdletsToExport" = $Exports.Cmdlets
        "FunctionsToExport" = $Exports.Functions
        "VariablesToExport" = $Exports.Variables
        "ModuleVersion" = $Version
    }

    if ($PSBoundParameters.ContainsKey("LicenseUri"))
    {
        $ManifestArguments.LicenseUri = $LicenseUri
    }

    if ($PSBoundParameters.ContainsKey("ProjectUri"))
    {
        $ManifestArguments.ProjectUri = $ProjectUri
    }

    if ($PSBoundParameters.ContainsKey("IconUri"))
    {
        $ManifestArguments.IconUri = $IconUri
    }

    if ($PSBoundParameters.ContainsKey("Tags") -and $Tags.Count -gt 0)
    {
        $ManifestArguments.Tags = $Tags
    }

    New-ModuleManifest -Path $Path @ManifestArguments

    if ($PSBoundParameters.ContainsKey("Prerelease"))
    {
        Update-ModuleManifest -Path $Path -Prerelease $Prerelease
    }
}
##### END Invoke-CreateModuleManifest.ps1 #####


##### BEGIN Invoke-GenerateSelfSignedCert.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-GenerateSelfSignedCert
{
    $AvailableCerts = @(Get-ChildItem -Path "Cert:\CurrentUser" -CodeSigningCert -Recurse).Where({ $_.Subject -eq "CN=Test Code Signing Certificate" -and (Test-Certificate -AllowUntrustedRoot -Cert $_) })
    $Certificate = $AvailableCerts | Sort-Object -Descending -Property NotAfter | Select-Object -First 1

    if ($null -eq $Certificate)
    {
        $CertArgs = @{
            Subject = "CN=Test Code Signing Certificate"
            KeyFriendlyName = "Test Code Signing Certificate"
            CertStoreLocation = "Cert:\CurrentUser"
            KeyAlgorithm = "RSA"
            KeyLength = 4096
            Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider"
            KeyExportPolicy = "NonExportable"
            KeyUsage = "DigitalSignature"
            Type = "CodeSigningCert"
            Verbose = $VerbosePreference
        }

        $Certificate = New-SelfSignedCertificate @CertArgs
        "Generated $Certificate"
    }

    $RootPath = "Cert:LocalMachine\Root"
    $TrustedRootEntry = @(Get-ChildItem -Path $RootPath -Recurse).Where({ $_.Thumbprint -eq $Certificate.Thumbprint }) | Select-Object -First 1
    if ($null -eq $TrustedRootEntry)
    {
        $ExportPath = Join-Path -Path $Env:TEMP -ChildPath "cert.crt"
        Export-Certificate -Type CERT -FilePath $ExportPath -Cert $Certificate -Force | Out-Null
        Import-Certificate -FilePath $ExportPath -CertStoreLocation $RootPath | Out-Null
        Remove-Item -Path $ExportPath

        "Copied $Certificate to trusted root"
    }
}
##### END Invoke-GenerateSelfSignedCert.ps1 #####


##### BEGIN Invoke-Sign.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-Sign
{
    param (
        [Parameter(Mandatory=$false)]
        [string]$CertificateThumbprint,

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

        [string]$CertificatePath,

        [securestring]$CertificatePassword,

        [string]$Name,

        [string]$Path,

        [string]$HashAlgorithm
    )

    if ($CertificatePath -like "Cert:*")
    {
        $Certificates = @(Get-ChildItem -Path $CertificatePath -CodeSigningCert)
    }
    else
    {
        $Certificates = @([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath, $CertificatePassword))
    }

    if (-not [string]::IsNullOrEmpty($CertificateThumbprint))
    {
        $Certificates = $Certificates.Where({ $_.Thumbprint -eq $CertificateThumbprint })
    }
    elseif (-not [string]::IsNullOrEmpty($CertificateSubject))
    {
        $Certificates = $Certificates.Where({ $_.Subject -eq $CertificateSubject })
    }

    $Script:Certificate = @($Certificates).Where({ $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) }) | Sort-Object -Descending -Property NotAfter | Select-Object -First 1
    if ($null -eq $Certificate)
    {
        throw "$($Certificates.Count) code signing certificates were found but none are valid."
    }

    $files = @(Get-ChildItem -Path $Path -Recurse -Include $ExtensionsToSign).ForEach({ $_.FullName })

    foreach ($file in $files) {
        $setAuthSigParams = @{
            FilePath = $file
            Certificate = $certificate
            HashAlgorithm = $HashAlgorithm
            Verbose = $VerbosePreference
        }

        $result = Set-AuthenticodeSignature @setAuthSigParams
        if ($result.Status -ne 'Valid') {
            throw "Failed to sign: $file. Status: $($result.Status) $($result.StatusMessage)"
        }

        "Successfully signed: $file"
    }

    $catalogFile = "$Path\$Name.cat"
    $catalogParams = @{
        Path = $Path
        CatalogFilePath = $catalogFile
        CatalogVersion = 2.0
        Verbose = $VerbosePreference
    }
    New-FileCatalog @catalogParams | Out-Null

    $catalogSignParams = @{
        FilePath = $catalogFile
        Certificate = $certificate
        HashAlgorithm = $HashAlgorithm
        Verbose = $VerbosePreference
    }
    $result = Set-AuthenticodeSignature @catalogSignParams
    if ($result.Status -ne 'Valid') {
        throw "Failed to sign the catalog file. Status: $($result.Status) $($result.StatusMessage)"
    }
}
##### END Invoke-Sign.ps1 #####


##### BEGIN Invoke-CreateHelp.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-CreateHelp
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Source,

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

        [string]$Language="en-US"
    )

    $destinationPath = Join-Path -Path $Destination -ChildPath $Language
    New-ExternalHelp -Path $Source -OutputPath $destinationPath -Force | Out-Null
}
##### END Invoke-CreateHelp.ps1 #####


##### BEGIN Invoke-CreateMarkdown.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-CreateMarkdown
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Manifest,

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

    $module = Import-Module -Name $Manifest -Global -Force -PassThru
    if (-not (Test-Path -Path $Path))
    {
        New-Item -Path $Path -ItemType Directory -Force | Out-Null
    }

    $moduleFile = Join-Path -Path $Path -ChildPath "$($module.Name).md"
    if (-not (Test-Path -Path $moduleFile))
    {
        New-MarkdownHelp -Module $($module.Name) -OutputFolder $Path -WithModulePage | Out-Null
    }

    Update-MarkdownHelpModule -Path $Path -RefreshModulePage | Out-Null
}
##### END Invoke-CreateMarkdown.ps1 #####


##### BEGIN Invoke-PublishToRepository.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-PublishToRepository
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Path,

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

        [Parameter(Mandatory=$false)]
        [string]$NugetApiKey
    )

    $publishParams = @{ "Path" = $Path }
    if (-not [string]::IsNullOrEmpty($Repository)) { $publishParams["Repository"] = $Repository }
    if (-not [string]::IsNullOrEmpty($NugetApiKey)) { $publishParams["NuGetApiKey"] = $NugetApiKey }

    Publish-Module @publishParams
}
##### END Invoke-PublishToRepository.ps1 #####


##### BEGIN Invoke-CodeAnalysis.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-CodeAnalysis
{
    param (
        [Parameter(Mandatory=$true)]
        [string]$Path,

        [string]$FailureLevel,

        [string]$SettingsFile
    )

    $analysisParameters = @{}
    if (-not [string]::IsNullOrEmpty($SettingsFile) -and (Test-Path -Path $SettingsFile)) { $analysisParameters["Settings"] = $SettingsFile }
    $analysisResult = Invoke-ScriptAnalyzer -Path $Path -Recurse @analysisParameters
    $analysisResult | Format-Table | Out-String -Width 192

    $warnings = $analysisResult.Where({ $_.Severity -eq "Warning" -or $_.Severity -eq "Warning" }).Count
    $errors = $analysisResult.Where({ $_.Severity -eq "Error" }).Count
    "Script analyzer triggered {0} warnings and {1} errors" -f $warnings, $errors

    if ($FailureLevel -eq "Warning")
    {
        Assert ($warnings -eq 0 -and $errors -eq 0) "Build failed due to warnings or errors found in analysis."
    }
    elseif ($FailureLevel -eq "Error")
    {
        Assert ($errors -eq 0) "Build failed due to errors found in analysis."
    }
}
##### END Invoke-CodeAnalysis.ps1 #####


##### BEGIN Invoke-PesterTest.ps1 #####
#.ExternalHelp PSBuilder-Help.xml
function Invoke-PesterTest
{
    param (
        [string[]]$Tags = @(),
        [string]$Path,
        [string]$Module,
        [string]$OutputPath,
        [string]$CoverageOutputPath,
        [int]$MinCoverage=0
    )

    if ($Tags -eq "*") { $Tags = @() }

    if (-not (Test-Path -Path $Path))
    {
        $testCoverage = 0
    }
    else
    {
        Set-Location -Path $Path
        Import-Module -Name $Module -Force
        $files = @(Get-ChildItem -Path ([IO.Path]::GetDirectoryName($Module)) -Include "*.ps1","*.psm1" -File -Recurse)
        $pesterArgs = @{
            CodeCoverage = $files
            Tag = $tags
            OutputFile = $OutputPath
            OutputFormat = "NUnitXml"
            CodeCoverageOutputFile = $CoverageOutputPath
            CodeCoverageOutputFileFormat = "JaCoCo"
            PassThru = $true
        }
        $testResult = Invoke-Pester @pesterArgs

        assert ($testResult.FailedCount -eq 0) ('Failed {0} Unit tests. Aborting Build' -f $testResult.FailedCount)

        if (0 -eq $testResult.CodeCoverage.NumberOfCommandsAnalyzed)
        {
            $testCoverage = 0
        }
        else
        {
            $testCoverage = [int]($testResult.CodeCoverage.NumberOfCommandsExecuted / $testResult.CodeCoverage.NumberOfCommandsAnalyzed * 100)
        }
    }

    Write-Output "Code coverage: ${testCoverage}%"

    assert ($MinCoverage -le $testCoverage) ('Code coverage must be higher or equal to {0}%. Current coverage: {1}%' -f ($MinCoverage, $testCoverage))
}
##### END Invoke-PesterTest.ps1 #####

Export-ModuleMember -Function @('Invoke-Builder', 'Invoke-CompileModule', 'Invoke-CreateModuleManifest', 'Invoke-GenerateSelfSignedCert', 'Invoke-Sign', 'Invoke-CreateHelp', 'Invoke-CreateMarkdown', 'Invoke-PublishToRepository', 'Invoke-CodeAnalysis', 'Invoke-PesterTest')