PSMake.psm1


                # Import System.Security from GAC if Windows PowerShell is being used
                if ( ([version]$PSVersionTable.PSVersion).Major -lt 6) {
                    $asm = [System.Reflection.Assembly]::LoadWithPartialName('System.Security')
                    if ($null -eq $asm) {
                        throw 'Unable to load System.Security from GAC for Windows PowerShell'
                    }
                }
            
. ([scriptblock]::Create(@'
function AddType {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Base64Encode', Justification='Used in ForEach-Object call')]
    param(
        [scriptblock]$Scriptblock,
        [string]$OutputDirectory = $settings.OutputDirectory,
        [string]$Filename = $settings.ModuleName + ".psm1",
        [switch]$Base64Encode
    )

    $content = & $Scriptblock | ForEach-Object {
        $current = $_
        if($current -is [string]) { Get-ChildItem $current }
        elseif($current -is [System.IO.FileInfo]) { $current }
        else { throw "Invalid object to collate - $_ of type '$($_.GetType())" }
    } | ForEach-Object {
        $content = [System.IO.File]::ReadAllText($_.FullName)
        $scriptblockText = $content
        if($Base64Encode) {
            $encodedScriptblock = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($scriptblockText))
            "Add-Type -TypeDefinition ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('$encodedScriptblock')))"
        } else {
            "Add-Type -TypeDefinition @'`r`n$scriptblockText`r`n'@`r`n"
        }
    }

    if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null }
    if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null }
    $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append
}
 
'@
))

. ([scriptblock]::Create(@'
using namespace System.Security.Cryptography.X509Certificates
using namespace System.IO

function CodeSign {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", 'BaseDirectory', Justification = "Used in ForEach-Object scriptblock")]
    Param(
        [ScriptBlock]$ScriptBlock,
        [string]$BaseDirectory = $settings.OutputModulePath,
        [string]$CertificatePath = 'Cert:\CurrentUser\My',
        [string]$TimestampServer = 'http://timestamp.comodoca.com/authenticode'
    )

    [X509Certificate2[]] $signingCerts = Get-ChildItem $CertificatePath -Recurse | Where-Object {
        $_ -is [X509Certificate2] -and ($_.EnhancedKeyUsageList.FriendlyName) -contains 'Code Signing'
    }

    if(-not $signingCerts) { throw "No Code Signing Certs Found!"}

    [X509Certificate2Collection]$selection = [X509Certificate2UI]::SelectFromCollection($signingCerts, "Select Certificate", "Select Code Signing Certificate", [X509SelectionFlag]::SingleSelection)

    if($selection.Count -ne 1) {
        throw 'No Code Signing Certificate Selected!'
    }

    [X509Certificate2]$cert = $selection[0]
    $authenticodeArgs = @{
        "Certificate" = $cert
        "IncludeChain" = 'all'
    }

    if($TimestampServer) {
        $authenticodeArgs["TimestampServer"] = $TimestampServer
    }

    $files = & $ScriptBlock | ForEach-Object {
        if($_ -is [string]) {
            Get-ChildItem (Resolve-Path (Join-Path $BaseDirectory $_)) -File
        } elseif($_ -is [FileInfo]) {
            $_
        } else {
            throw "Item $_ neither a path or file. Cannot Code Sign"
        }
    }

    $authenticodeArgs["FilePath"] = $files.FullName

    Set-AuthenticodeSignature @authenticodeArgs | Out-Null
}
'@
))

. ([scriptblock]::Create(@'
function Collate {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", 'Base64Encode', Justification = "Used in ForEach-Object scriptblock")]
    param(
        [scriptblock]$Scriptblock,
        [string]$OutputDirectory = $settings.OutputDirectory,
        [string]$Filename = $settings.ModuleName + ".psm1",
        [switch]$Base64Encode
    )

    $content = & $Scriptblock | ForEach-Object {
        $current = $_
        if($current -is [string]) { Get-ChildItem $current }
        elseif($current -is [System.IO.FileInfo]) { $current }
        else { throw "Invalid object to collate - $_ of type '$($_.GetType())" }
    } | ForEach-Object {
        $content = [System.IO.File]::ReadAllText($_.FullName)
        $scriptblockText = $content
        if($Base64Encode) {
            $encodedScriptblock = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($scriptblockText))
            ". ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('$encodedScriptblock'))))"
        } else {
            ". ([scriptblock]::Create(@'`r`n$scriptblockText`r`n'@))`r`n"
        }
    }

    if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null }
    if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null }
    $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append
}
 
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO;

function CopyDirectory {
    Param(
        [ScriptBlock]$ScriptBlock,
        [string]$To = $settings.OutputModulePath
    )

    if($To -ne $settings.OutputModulePath -and -not (Split-Path $To -IsAbsolute)) {
        $To = Join-Path $settings.OutputModulePath $To
    }

    & $ScriptBlock | ForEach-Object {
        if($_ -is [string]) {
            if(Split-Path $_ -IsAbsolute) {
                $_
            } else {
                (Join-Path $PWD.Path $_)
            }
        } elseif($_ -is [DirectoryInfo]) {
            $_.FullName
        } else {
            throw "Unexpected directory to copy - '$_'"
        }
    } | ForEach-Object {
        Copy-Item $_ $To -Recurse
    }
}
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO

function CopyFiles {
    param(
        [scriptblock]$ScriptBlock,
        [string]$To = $settings.OutputModulePath
    )

    if($To -ne $settings.OutputModulePath -and -not (Split-Path $To -IsAbsolute)) {
        $To = Join-Path $settings.OutputModulePath $To
    }

    & $ScriptBlock | ForEach-Object {
        if($_ -is [string]) {
            Get-ChildItem $_
        } elseif($_ -is [FileInfo]) {
            $_
        } else {
            throw "Unexpected item to copy - '$_'"
        }
    } | ForEach-Object {
        Copy-Item $_.FullName $To
    }
}
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO

function CreateDirectory {
    param(
        [scriptblock]$ScriptBlock,
        [string]$In = $settings.OutputModulePath
    )
    $fullIn = (Resolve-Path $In).Path
    & $ScriptBlock | ForEach-Object {
        if($_ -is [string]) {
            [DirectoryInfo]::new([Path]::Combine($fullIn, $_))
        } elseif($_ -is [System.IO.DirectoryInfo]) {
            $_
        } else {
            throw "Unexpected item to copy - '$_'"
        }
    } | ForEach-Object {
        if(-not $_.Exists) { $_.Create() }
    }
}
'@
))

. ([scriptblock]::Create(@'
function CustomCode {
    param(
        [scriptblock]$Scriptblock,
        [string]$OutputDirectory = $settings.OutputDirectory,
        [string]$Filename = $settings.ModuleName + ".psm1"
    )

    $content = $Scriptblock.ToString()

    if(-not (Test-Path $OutputDirectory -PathType Container)) {
        New-Item $OutputDirectory -ItemType Directory | Out-Null
    }

    if(-not (Test-Path $settings.OutputModulePath -PathType Container)) {
        New-Item $settings.OutputModulePath -ItemType Directory | Out-Null
    }

    $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append
}
 
'@
))

. ([scriptblock]::Create(@'
function Debug {
    Param(
        [scriptblock]$ScriptBlock,
        [string]$BuildTarget = $settings.BuildTarget
    )

    if($BuildTarget -eq "Debug") {
        & $ScriptBlock
    }
}
'@
))

. ([scriptblock]::Create(@'
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'BuildSetting is weird and this is used internally only')]
param()

function Get-BuildSettings {
    [OutputType([Hashtable])]
    param(
        [string]$BuildFilePath,
        [string]$BuildTarget
    )

    if(-not (test-path $BuildFilePath)) { throw "No $($BuildFilePath.Substring(2)) file found!" }

    $defaultData = Import-PowerShellDataFile (Join-Path $MyInvocation.MyCommand.Module.ModuleBase "defaultsettings.psd1")
    $buildSettings = @{}
    $buildSettingsAsConfigured = Import-PowerShellDataFile $BuildFilePath

    foreach($defaultKey in $defaultData.Keys) {
        $buildSettings.Add($defaultkey, $defaultData[$defaultKey])
    }

    foreach($asConfiguredKey in $buildSettingsAsConfigured.Keys) {
        $buildSettings[$asConfiguredKey] = $buildSettingsAsConfigured[$asConfiguredKey]
    }

    $buildSettings["BuildTarget"] = if($PSBoundParameters.ContainsKey("BuildTarget")) {
        $BuildTarget
    } elseif($buildSettings.ContainsKey("DefaultBuildTarget")) {
        $buildSettings["DefaultBuildTarget"]
    } else {
        "Release"
    }

    $buildSettings["BuildTargetPath"] = Join-Path $buildSettings["OutputDirectory"] $buildSettings["BuildTarget"]

    if(-not $buildSettingsAsConfigured.ContainsKey("OutputModulePath")) {
        $buildSettings.Add("OutputModulePath", (Join-Path $buildSettings["BuildTargetPath"] $buildSettings["ModuleName"]))
    }

    $credential = GetBuildCredential -Settings $buildSettings
    Write-Verbose "Credential - $credential"
    if ($credential) {
        $buildSettings.Credential = $credential
    }

    # Check for required parameters
    Validate-BuildSettings $buildSettings
    $buildSettings
}
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO

function GetBuildCredential {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConverttoSecureStringWithPlainText', '', Justification = '-AsPlainText is required on *nix systems')]
    [CmdletBinding()]
    [OutputType([PSCredential])]
    param(
        $Settings = $settings
    )

    # Environment wins ALWAYS
    if ($env:POWERSHELL_REPO_USR -and $env:POWERSHELL_REPO_PW) {
        return [pscredential]::new($env:POWERSHELL_REPO_USR, (ConvertTo-SecureString -String $env:POWERSHELL_REPO_PW -AsPlainText -Force))
    }

    # Custom set Credential
    if($null -ne $Settings.Credential) {
        if ($Settings.Credential -is [scriptblock]) {
            # CustomFactory
            Write-Verbose "Credential is a scriptblock, Invoking now."
            $output = $Settings.Credential.InvokeWithContext($null, [psvariable]::new("settings", $Settings), $null)
            if (-not $output -or -not ($output -is [pscredential])) {
                throw "Credential Factory did not return a PSCredential object!"
            }
            return $output
        }
        elseif ($Settings.Credential -is [string]) {
            Write-Verbose 'Credential is a string. Checking path.'
            # Configured Path
            if (-not (test-path $Settings.Credential -PathType Leaf)) {
                throw "Credential path '$($Settings.Credential)' does not exist or is not a file!"
            }

            [FileInfo]$fileInfo = [FileInfo]::new($Settings.Credential)

            $obj = switch($fileInfo.Extension.ToLower()) {
                ".json" {
                    Write-Verbose 'JSON credential detected.'
                    Get-Content $Settings.Credential -Raw | ConvertFrom-Json -ErrorAction Stop
                }
                ".xml" {
                    Write-Verbose 'CliXml credential detected.'
                    Import-CliXml $Settings.Credential -ErrorAction Stop
                }
                default {
                    throw "Credential in file '$($Settings.Credential)' not in json or xml format!"
                }
            }

            $usernameProperty = $obj.PSObject.Properties.Match("UserName")
            $passwordProperty = $obj.PSObject.Properties.Match("Password")

            if ($null -eq $usernameProperty -or $null -eq $passwordProperty) {
                throw "Credential in file '$($Settings.Credential)' missing required username and password properties!"
            }

            return [pscredential]::new($usernameProperty.Value, (ConvertTo-SecureString -String $passwordProperty.Value -AsPlainText -Force))
        }
        elseif ($Settings.Credential -is [PSCredential]) {
            Write-Verbose 'Credential property is already a PSCredential.'
            return $Settings.Credential
        }
        else {
            throw 'Restore Credential is not a factory, file path, or PSCredential!'
        }
    }
    Write-Verbose 'No credential detected. Returning null.'
    # Default - $null
    return $null
}
'@
))

. ([scriptblock]::Create(@'
function GetPlatformEnvironment {
    [CmdletBinding()]
    param()

    [System.OperatingSystem]$OS = [System.Environment]::OSVersion

    return [PSCustomObject]@{
        Platform = $OS.Platform
        OSVersion = [Version]($OS.Version)
        OSVersionString = $OS.VersionString
        PSVersion = [Version]($PSVersionTable.PSVersion)
    }
}
'@
))

. ([scriptblock]::Create(@'
function GetRestoredDependency {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ModuleName,
        [string]$ModuleVersion,
        [string]$RequiredVersion,
        [Parameter(Mandatory = $true)]
        [string]$PSModuleDirectory
    )

    if (-not (Test-Path $PSModuleDirectory -PathType Container)) {
        throw "PSModuleDirectory path '$PSModuleDirectory' does not exist or is not a folder."
    }

    $requiredVersion = $null
    if($RequiredVersion) {
        [Version]::TryParse($RequiredVersion, [ref]$requiredVersion) | Out-Null
    }

    $moduleVersion = $null
    if ($ModuleVersion) {
        [Version]::TryParse($ModuleVersion, [ref]$moduleVersion) | Out-Null
    }

    Get-ChildItem -Path (Join-Path $PSModuleDirectory $ModuleName) -Directory -ErrorAction SilentlyContinue | `
    Where-Object {
        $Version = $null
        if ([Version]::TryParse($_.Name, [ref]$Version)) {
            if($requiredVersion) {
                $Version -eq $requiredVersion
            }
            elseif ($moduleVersion) {
                $Version -ge $moduleVersion -and $Version.Major -eq $moduleVersion.Major
            }
            else {
                $true
            }
        }
        else {
            $false
        }
    } | `
    Sort-Object -Descending -Property "Name" | `
    Select-Object -First 1 | `
    Select-Object -ExpandProperty FullName | `
    ForEach-Object {
        Import-PowerShellDataFile -Path (Join-Path $_ "$ModuleName.psd1") -ErrorAction SilentlyContinue | `
        ForEach-Object {
            $_.Add("Name", $ModuleName)
            $_.Add("Version", $_.ModuleVersion)
            Write-Output $_
        }
    }
}
 
'@
))

. ([scriptblock]::Create(@'
using namespace System.Management.Automation
using namespace System.Collections.ObjectModel
using namespace System

function Invoke-PSMake {
    [CmdletBinding()]
    param(
        [ValidateSet("","build", "clean", "test", "template", "publish")]
        [string]$Command = "build"
    )

    DynamicParam {

        if ($Command -eq 'template') {
            $paramDictionary = [RuntimeDefinedParameterDictionary]::new()
            $attributeCollection = [Collection[Attribute]]::new()

            $projectNameParameterAttribute = [ParameterAttribute]@{
                Mandatory = $true
                Position = 2
            }

            $attributeCollection.Add($projectNameParameterAttribute)
            $projectNameParam = [RuntimeDefinedParameter]::new('ProjectName', [string], $attributeCollection)
            $paramDictionary.Add("ProjectName", $projectNameParam)
            return $paramDictionary
        }

        if ($Command -eq 'publish') {
            $paramDictionary = [RuntimeDefinedParameterDictionary]::new()
            $attributeCollection = [Collection[Attribute]]::new()
            $nugetApiKeyAttribute = [ParameterAttribute]@{
                Position = 2
            }
            $attributeCollection.Add($nugetApiKeyAttribute)
            $nugetApiKeyParam = [RuntimeDefinedParameter]::new('NuGetApiKey', [string], $attributeCollection)
            $paramDictionary.Add("NuGetApiKey", $nugetApiKeyParam)

            $buildTargetAttributeCollection = [Collection[Attribute]]::new()
            $buildTargetParameterAttribute = [ParameterAttribute]@{
                Position = 3
            }
            $buildTargetAttributeCollection.Add($buildTargetParameterAttribute)
            $buildTargetParam = [RuntimeDefinedParameter]::new('BuildTarget', [string], $buildTargetAttributeCollection)
            $buildTargetParam.Value = "Release"
            $PSBoundParameters["BuildTarget"] = $buildTargetParam.Value
            $paramDictionary.Add("BuildTarget", $buildTargetParam)
            return $paramDictionary
        }

        if ($Command -eq 'test') {
            $paramDictionary = [RuntimeDefinedParameterDictionary]::new()
            $attributeCollection = [Collection[Attribute]]::new()
            $reportsParameterAttribute = [ParameterAttribute]@{
                Position = 2
            }
            $attributeCollection.Add($reportsParameterAttribute)
            $reportsParam = [RuntimeDefinedParameter]::new('ReportType', [string], $attributeCollection)
            $paramDictionary.Add("ReportType", $reportsParam)
            return $paramDictionary
        }

        if ($Command -eq 'build') {
            $paramDictionary = [RuntimeDefinedParameterDictionary]::new()
            $attributeCollection = [Collection[Attribute]]::new()
            $buildTargetParameterAttribute = [ParameterAttribute]@{
                Position = 2
            }
            $attributeCollection.Add($buildTargetParameterAttribute)
            $buildTargetParam = [RuntimeDefinedParameter]::new('BuildTarget', [string], $attributeCollection)
            $buildTargetParam.Value = "Release"
            $PSBoundParameters["BuildTarget"] = $buildTargetParam.Value
            $paramDictionary.Add("BuildTarget", $buildTargetParam)
            return $paramDictionary
        }
    }

    Begin {
        $invokeArgs = @()
    }

    Process{
        $PSBoundParameters.Keys | ForEach-Object { Write-Verbose "Bound Parameter: $_ = $($PSBoundParameters[$_])" }
        switch($Command) {

            "template" {
                $path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PSBoundParameters["ProjectName"])
                $name = Split-Path $path -Leaf
                Write-Verbose "Project Directory: $path"
                Write-Verbose "Project Name: $name"

                if (-not (Test-Path $path -PathType Container)) {
                    Write-Verbose "Creating $path"
                    New-Item -Path $path -ItemType Directory -Force | Out-Null
                }

                if (-not (test-path (Join-Path $path "functions") -PathType Container)) {
                    Write-Verbose "Creating $(Join-Path $path "functions")"
                    New-Item -Path (Join-Path $path "functions") -ItemType Directory -Force | Out-Null
                }

                if (-not (test-path (Join-Path $path "tests") -PathType Container)) {
                    Write-Verbose "Creating $(Join-Path $path "tests")"
                    New-Item -Path (Join-Path $path "tests") -ItemType Directory | Out-Null
                }

                Write-Verbose "Generating $(Join-Path $path "$name.psm1")"
                'Get-ChildItem $PSScriptRoot\functions\ -File -Recurse | ForEach-Object { . $_.FullName }' | Out-File (Join-Path $path "$name.psm1") -Encoding ASCII

                $dom = $env:userdomain
                $usr = $env:username

                try {
                    Write-Verbose "Getting user full name from active directory - User - $usr, Domain -$dom"
                    $author = ([adsi]"WinNT://$dom/$usr,user").fullname.ToString()
                } catch {
                    $author = "$usr$(if($dom) { "@$dom" })"
                    Write-Verbose "Failed, using author '$author'"
                }

                if(-not (test-path (Join-Path $path "$name.psd1") -PathType Leaf)) {
                    Write-Verbose "Generating manifest $(Join-Path $path "$name.psd1")"
                    New-ModuleManifest -Path (Join-Path $path "$name.psd1") -CompanyName "USAF, 38 CEIG/ES" -Copyright "GOTS" -RootModule "$name.psm1" -ModuleVersion "1.0.0.0" -Author $author
                }

                Write-Verbose "Generating $(Join-Path $path "build.psd1")"
                (Get-Content "$($MyInvocation.MyCommand.Module.ModuleBase)\template.psd1").Replace("%%MODULENAME%%", $name) | Out-File (Join-Path $path "build.psd1")

                Write-Verbose "Copying Pester5Configuration-local.psd1 and Pester5Configuration-cicd.psd1"
                Copy-Item "$($MyInvocation.MyCommand.Module.ModuleBase)\Pester5Configuration-local.psd1" (Join-Path $path "Pester5Configuration-local.psd1")
                Copy-Item "$($MyInvocation.MyCommand.Module.ModuleBase)\Pester5Configuration-cicd.psd1" (Join-Path $path "Pester5Configuration-cicd.psd1")
            }

            {$_ -ne "template" } {
                $buildArgs = @{
                    BuildFilePath = ".\build.psd1"
                }

                if($PSBoundParameters.ContainsKey("BuildTarget")) {
                    $buildArgs.Add("BuildTarget", $PSBoundParameters["BuildTarget"])
                }

                $buildData = Get-BuildSettings @buildArgs
                $settings = @{}
                $buildData.Keys | Where-Object { -not ($_ -in "build","clean","test","template","publish") } | ForEach-Object { $settings[$_] = $buildData[$_] }

            }

            { $_ -in "","build" } {
                if(-not (test-path $buildData.OutputDirectory -PathType Container)) { New-Item $buildData.OutputDirectory -ItemType Directory | Out-Null }
                if(-not (test-path $buildData.OutputModulePath -PathType Container)) { New-Item $buildData.OutputModulePath -ItemType Directory | Out-Null }
                $invokeArgs += $PSBoundParameters["BuildTarget"]
            }

            { $_ -in "publish", "test"} {
                ${function:RestoreDependencies}.InvokeWithContext($null, [PSVariable]::new("settings", $settings), $null)
            }

            { $_ -eq 'publish' } {
                if($PSBoundParameters.ContainsKey("NuGetApiKey")) {
                    $invokeArgs += $PSBoundParameters["NuGetApiKey"]
                }
            }

            { $_ -eq "test" } {
                if($PSBoundParameters.ContainsKey("ReportType")) {
                    $invokeArgs += $PSBoundParameters["ReportType"]
                }
            }

            {$_ -in "", "build","clean","test","publish" } {
                if(-not $buildData.ContainsKey($Command)) { throw "Unable to run '$Command' due to build.psd1 not containing a '$Command' scriptblock" }
                if($buildData.ContainsKey("DevRequiredModules")) { RestoreDependencies -RequiredModule $buildData["DevRequiredModules"] }
                InvokeWithPSModulePath -NewPSModulePath $settings.RestoredDependenciesPath -ScriptBlock { (& $buildData[$Command]).InvokeWithContext($null, [PSVariable]::new('settings', $settings), $invokeArgs) }.GetNewClosure()
            }

            default {
                throw "Undefined command '$Command'"
            }
        }
    }


    <#
        .SYNOPSIS
        Invokes the PSMake project management tool based on given parameter (defaults to build release)

        .DESCRIPTION
        Builds a PSMake structured project in the current directory based on the build.psd1 file (build, test, clean, plublish)
        or creates the project structure with default settings (template)

        .PARAMETER Command
        Specifies the action to take (build, test, clean, publish, template)

        .INPUTS
        None. Piping unavailable.

        .OUTPUTS
        None. Affects project outputs and runs other test scripts based on the build.psd1 file.

        .EXAMPLE
        PS> PSMake
        # builds a release version of the module specified within build.psd1

        PS> PSMake build release
        # same as above, but explicit

        .EXAMPLE
        PS> PSMake build debug
        # builds the debug version of the module as specified within the build.psd1 Build property-script

        .EXAMPLE
        PS> PSMake clean
        # runs the 'Clean' property-script within build.psd1 (deletes the dist/ folder by default)

        .EXAMPLE
        PS> PSMake test
        # runs the 'Test' property-script within the build.psd1 file

        PS> PSMake test reports
        # runs the 'Test' property-script within the build.psd1 file and passes "reports" value as a parameter to it.

        .EXAMPLE
        PS> PSMake publish
        # runs the 'Publish' property-script within the build.psd1 file

        .EXAMPLE
        PS> PSMake template
        # Initializes a new PSMake project with templated build.psd1, module file, module manifest, specialized folders
    #>
}

New-Alias -Name "psmake" Invoke-PSMake -ErrorAction SilentlyContinue

Export-ModuleMember -Function 'Invoke-PSMake'
Export-ModuleMember -Alias "psmake"
 
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO

function InvokeWithPSModulePath {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ScriptBlock', Justification = 'ScriptBlock is used within a child scriptblock')]
    [CmdletBinding()]
    param(
        [string]$NewPSModulePath,
        [ScriptBlock]$ScriptBlock
    )

    $pe = GetPlatformEnvironment

    $seperatorChar = switch($pe.Platform) {
        { $_ -in [System.PlatformID]::MacOSX, [System.PlatformID]::Unix } {
            ':'
        }
        default {
            ";"
        }
    }

    $cache = $env:PSModulePath
    $newList = @()
    $newList += ([System.IO.Path]::GetFullPath($NewPSModulePath))
    $cache.Split($seperatorChar) | ForEach-Object { $newList += $_ }

    $env:PSModulePath = [string]::Join($seperatorChar, $newList)
    Write-Verbose "PSModulePath = $($env:PSModulePath)"
    $ps = [PowerShell]::Create([System.Management.Automation.RunspaceMode]::CurrentRunspace)
    try {

        $ps.AddScript("`$env:PSModulePath='$($env:PSModulePath)'") | Out-Null

        $ps.AddCommand("Invoke-Command") | Out-Null
        $ps.AddParameter("ScriptBlock", { . $MyInvocation.MyCommand.Module $ScriptBlock }.GetNewClosure()) | Out-Null

        $ps.Invoke()

        if($ps.HadErrors) {
            $ps.Streams.Error | ForEach-Object { Write-Error $_; $_.InvocationInfo.PositionMessage }
        }
    }
    finally {
        $ps.Dispose()
        $env:PSModulePath = $cache
    }
}
'@
))

. ([scriptblock]::Create(@'
function Prerelease {
    Param(
        [scriptblock]$ScriptBlock,
        [string]$BuildTarget = $settings.BuildTarget
    )

    if($BuildTarget -eq "Prerelease") {
        & $ScriptBlock
    }
}
'@
))

. ([scriptblock]::Create(@'
function Release {
    Param(
        [scriptblock]$ScriptBlock,
        [switch]$AndPrerelease,
        [string]$BuildTarget = $settings.BuildTarget
    )

    if(($PSBoundParameters.ContainsKey("AndPrerelease") -and $BuildTarget -in "Release", "Prerelease") -or $BuildTarget -eq "Release") {
        & $ScriptBlock
    }
}
'@
))

. ([scriptblock]::Create(@'
using namespace System.IO

function RestoreDependencies {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AllowPrerelease', Justification = 'Used in a ForEach-Object scriptblock')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Credential', Justification = 'Used in a ForEach-Object scriptblock')]
    [CmdletBinding()]
    param(
        $RequiredModules = (Import-PowerShellDataFile "$($settings.ModuleName).psd1").RequiredModules,
        $OutputDirectory = ".dependencies",
        $AllowPrerelease = $false,
        [pscredential]$Credential = $settings.Credential
    )

    if (-not $RequiredModules) { return }

    if (-not (test-path $OutputDirectory)) {
        new-item -Path $OutputDirectory -ItemType Directory | Out-Null
    }

    # Ensure dependencies are installed before importing the module
    Write-Verbose "Restoring Dependencies..."
    $RequiredModules | ForEach-Object {
        $module = $_
        $moduleInfo = @{}
        if ($module -is [string]) {
            $moduleInfo.Add("Name", $module)
        }
        else {
            $moduleInfo.Add("Name", $module.ModuleName)
            if($module.ContainsKey("ModuleVersion")) {
                $moduleInfo.Add("MinimumVersion", $module.ModuleVersion)
            }
            elseif ($module.ContainsKey("RequiredVersion")) {
                $moduleInfo.Add("RequiredVersion", $module.RequiredVersion)
            }
        }

        if ($PSBoundParameters.ContainsKey("AllowPrerelease")) {
            $moduleInfo.Add("AllowPrerelease", $AllowPrerelease)
        }

        $moduleInfo.Add("ErrorAction", "Stop")

        if($Credential) {
            $moduleInfo.Add("Credential", $Credential)
        }

        $cachedModuleInfo = @{
            PSModuleDirectory = $OutputDirectory
        }

        $module.Keys | ForEach-Object { $cachedModuleInfo.Add($_, $module[$_]) }

        $foundModule = GetRestoredDependency @cachedModuleInfo

        if ($null -eq $foundModule) {
            Write-Verbose "Restoring Module '$($moduleInfo.Name)'$(if($moduleInfo.RequiredVersion) { ", RequiredVersion = $($moduleInfo.RequiredVersion)"})$(if($moduleInfo.MinimumVersion) { ", MinimumVersion = $($moduleInfo.MinimumVersion)"})$(if($Credential) { " using username $($Credential.UserName)" })"
            $foundModule = Find-Module @moduleInfo | Select-Object -First 1
            $installedModulePath = [Path]::Combine($OutputDirectory, $foundModule.Name, $foundModule.Version.Split('-')[0])
            $installedModuleInfoPath = [Path]::Combine($installedModulePath, 'PSGetModuleInfo.xml')

            if (-not (test-path $installedModuleInfoPath) -or $foundModule.Version -ne ((Import-CliXml $installedModulePath\PSGetModuleInfo.xml)).Version) {

                Write-Verbose "Module $($foundModule.Name) ($($foundModule.Version)) not installed... installing."
                $SaveArgs = @{
                    Name = $foundModule.Name
                    Path = $OutputDirectory
                    RequiredVersion = $foundModule.Version.ToString()
                    Repository = $foundModule.Repository
                    ErrorAction = 'Stop'
                    Force = $true
                }
                if ($AllowPrerelease) {
                    $SaveArgs.Add("AllowPrerelease", $AllowPrerelease)
                }
                if ($Credential) {
                    $SaveArgs.Add("Credential", $Credential)
                }
                Save-Module @SaveArgs
            }
        }

        Write-Verbose "Using Module - $($foundModule.Name) $($foundModule.Version)"
    }
}
 
'@
))

. ([scriptblock]::Create(@'
function SetPrereleaseTag {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Tag', Justification = 'Used in a ForEach-Object scriptblock')]
    Param(
        [scriptblock]$ScriptBlock,
        [string]$Tag = "rc$((Get-Date).ToString("yyyyMMddHHmm"))"
    )

    & $ScriptBlock | ForEach-Object {
        $path = Join-Path $settings.OutputModulePath $_

        if (-not (Test-Path $path -PathType Leaf)) {
            throw "Path '$_' is not file"
        }

       Update-Metadata $path -PropertyName "PrivateData.PSData.Prerelease" -Value $Tag

    }
}
'@
))

. ([scriptblock]::Create(@'
function UsingModule {
    param(
        [scriptblock]$Scriptblock,
        [string]$OutputDirectory = $settings.OutputDirectory,
        [string]$Filename = $settings.ModuleName + ".psm1"
    )

    $content = & $Scriptblock | ForEach-Object {
        $current = $_
        if($current -is [string]) { $current }
        else { throw "Invalid object - expected string of module to use - $_ of type '$($_.GetType())" }
    } | ForEach-Object {
        "using module `"$_`"`r`n"
    }

    if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null }
    if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null }
    $content + (Get-Content (Join-Path $settings.OutputModulePath $Filename) -ErrorAction SilentlyContinue) | Out-File (Join-Path $settings.OutputModulePath $Filename)
}
 
'@
))

. ([scriptblock]::Create(@'
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Scope='Function', Justification = 'BuildSettings is a type')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope='Function', Justification = 'BuildSettings is a type')]
param()

function Validate-BuildSettings {
    param(
        [hashtable]$Settings
    )

    # Check for module name
    if(-not $Settings.ContainsKey("ModuleName")) { throw "Required Property 'ModuleName' is not defined." }
    if(-not ($Settings["ModuleName"] -match "^[a-zA-Z][a-zA-Z0-9-_]*$")) { throw "Property 'ModuleName' ($($Settings["ModuleName"])) is invalid." }

    # Check for Build scriptblock
    if(-not $Settings.ContainsKey("Build")) { throw "Required Property 'Build' is not defined" }
    if(-not $Settings["Build"] -is [scriptblock]) { throw "Property Build is not a scriptblock! ($($Settings.Build))" }

    # Check for Build scriptblock
    if(-not $Settings.ContainsKey("Clean")) { throw "Required Property 'Clean' is not defined" }
    if(-not $Settings["Clean"] -is [scriptblock]) { throw "Property Clean is not a scriptblock! ($($Settings.Clean))" }

    # Check if Valid BuildTarget
    if(-not $Settings.ContainsKey("BuildTarget")) { throw "Required Property 'BuildTarget' is not defined" }
    if(-not $Settings["BuildTarget"] -is [string]) { throw "Property 'BuildTarget' not a string! ($($Settings.BuildTarget))" }
    if(-not @("Release", "Prerelease", "Debug") -contains $Settings["BuildTarget"]) { throw "Property 'BuildTarget' is not a valid build target (Release, Debug)! ($($Settings.BuildTarget))" }
}
'@
))