PlasterTemplate/build.ps1

<#
 
.DESCRIPTION
 Bootstrap and build script for PowerShell module pipeline
 
#>

[CmdletBinding()]
param(

    [Parameter(Position = 0)]
    [string[]]$Tasks = '.',

    [Parameter()]
    [validateScript(
        { Test-Path -Path $_ }
    )]
    $BuildConfig = './build.yaml',

    # A Specific folder to build the artefact into.
    [Parameter()]
    $OutputDirectory = 'output',

    # Can be a path (relative to $PSScriptRoot or absolute) to tell Resolve-Dependency & PSDepend where to save the required modules,
    # or use CurrentUser, AllUsers to target where to install missing dependencies
    # You can override the value for PSDepend in the Build.psd1 build manifest
    # This defaults to $OutputDirectory/modules (by default: ./output/modules)
    [Parameter()]
    $RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'),

    # Filter which tags to run when invoking Pester tests
    # This is used in the Invoke-Pester.pester.build.ps1 tasks
    [string[]]
    [parameter()]
    $PesterTag,

    # Filter which tags to exclude when invoking Pester tests
    # This is used in the Invoke-Pester.pester.build.ps1 tasks
    [string[]]
    [parameter()]
    $PesterExcludeTag,

    $CodeCoverageThreshold = 50,

    [Parameter()]
    [Alias('bootstrap')]
    [switch]$ResolveDependency,

    [parameter(DontShow)]
    [AllowNull()]
    $BuildInfo
)

# The BEGIN block (at the end of this file) handles the Bootstrap of the Environment before Invoke-Build can run the tasks
# if the -ResolveDependency (aka Bootstrap) is specified, the modules are already available, and can be auto loaded

Process {

    if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') {
        # Only run the process block through InvokeBuild (Look at the Begin block at the bottom of this script)
        return
    }

    # Execute the Build Process from the .build.ps1 path.
    Push-Location -Path $PSScriptRoot -StackName BeforeBuild

    try {
        Write-Host -ForeGroundColor magenta "[build] Parsing defined tasks"

        # Load Default BuildInfo if not provided as parameter
        if (!$PSBoundParameters.ContainsKey('BuildInfo')) {
            try {
                if (Test-Path $BuildConfig) {
                    $ConfigFile = (Get-Item -Path $BuildConfig)
                    Write-Host "[build] Loading Configuration from $ConfigFile"
                    $BuildInfo = switch -Regex ($ConfigFile.Extension) {
                        # Native Support for PSD1
                        '\.psd1' {
                            Import-PowerShellDataFile -Path $BuildConfig
                        }
                        # Support for yaml when module PowerShell-Yaml is available
                        '\.[yaml|yml]' {
                            Import-Module -ErrorAction Stop -Name 'powershell-yaml'
                            ConvertFrom-Yaml -Yaml (Get-Content -Raw $ConfigFile)
                        }
                        # Native Support for JSON and JSONC (by Removing comments)
                        '\.[json|jsonc]' {
                            $JSONC = (Get-Content -Raw -Path $ConfigFile)
                            $JSON = $JSONC -replace '(?m)\s*//.*?$' -replace '(?ms)/\*.*?\*/'
                            # This should probably be converted to hashtable for splatting
                            $JSON | ConvertFrom-Json
                        }
                        default {
                            Write-Error "Extension '$_' not supported. using @{}"
                            @{ }
                        }
                    }
                }
                else {
                    Write-Host -Object "Configuration file $BuildConfig not found" -ForegroundColor Red
                    $BuildInfo = @{ }
                }
            }
            catch {
                Write-Host -Object "Error loading data from $ConfigFile" -ForegroundColor Red
                $BuildInfo = @{ }
                Write-Error $_.Exception.Message
            }
        }

        # If the Invoke-Build Task Header is specified in the Build Info, set it
        if ($BuildInfo.TaskHeader) {
            Set-BuildHeader ([scriptblock]::Create($BuildInfo.TaskHeader))
        }

        # Import Tasks from modules via their exported aliases when defined in BUild Manifest
        # https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import#example-2-import-from-a-module-with-tasks
        if ($BuildInfo.containsKey('ModuleBuildTasks')) {
            foreach ($Module in $BuildInfo['ModuleBuildTasks'].Keys) {
                try {
                    $LoadedModule = Import-Module $Module -PassThru -ErrorAction Stop
                    foreach ($TaskToExport in $BuildInfo['ModuleBuildTasks'].($Module)) {
                        $LoadedModule.ExportedAliases.GetEnumerator().Where{
                            # using -like to support wildcard
                            Write-Host -ForegroundColor DarkGray "`t Loading $($_.Key)..."
                            $_.Key -like $TaskToExport
                        }.ForEach{
                            # Dot sourcing the Tasks via their exported aliases
                            . (get-alias $_.Key)
                        }
                    }
                }
                catch {
                    Write-Host -ForegroundColor Red -Object "Could not load tasks for module $Module."
                    Write-Error $_
                }
            }
        }

        # Loading Build Tasks defined in the .build/ folder (will override the ones imported above if same task name)
        Get-ChildItem -Path ".build/" -Recurse -Include *.ps1 -ErrorAction Ignore | Foreach-Object {
            "Importing file $($_.BaseName)" | Write-Verbose
            . $_.FullName
        }

        # Synopsis: Empty task, useful to test the bootstrap process
        task noop { }

        # Define default task sequence ("."), can be overridden in the $BuildInfo
        task . {
            Write-Build Yellow "No sequence currently defined for the default task"
        }

        # Load Invoke-Build task sequences/workflows from $BuildInfo
        foreach ($Workflow in $BuildInfo.BuildWorkflow.keys) {
            Write-Verbose "Creating Build Workflow '$Workflow' with tasks $($BuildInfo.BuildWorkflow.($Workflow) -join ', ')"
            $WorkflowItem = $BuildInfo.BuildWorkflow.($Workflow)
            if ($WorkflowItem.Trim() -match '^\{(?<sb>[\w\W]*)\}$') {
                $WorkflowItem = [ScriptBlock]::Create($Matches['sb'])
            }
            Write-Host -ForegroundColor DarkGray "Adding $Workflow"
            task $Workflow $WorkflowItem
        }

        Write-Host -ForeGroundColor magenta "[build] Executing requested workflow: $($Tasks -join ', ')"

    }
    finally {
        Pop-Location -StackName BeforeBuild
    }
}

Begin {
    # Bootstrapping the environment before using Invoke-Build as task runner

    if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') {
        Write-Host -foregroundColor Green "[pre-build] Starting Build Init"
        Push-Location $PSScriptRoot -StackName BuildModule
    }

    if ($RequiredModulesDirectory -in @('CurrentUser', 'AllUsers')) {
        # Installing modules instead of saving them
        Write-Host -foregroundColor Green "[pre-build] Required Modules will be installed for $RequiredModulesDirectory, not saved."
        # Tell Resolve-Dependency to use provided scope as the -PSDependTarget if not overridden in Build.psd1
        $PSDependTarget = $RequiredModulesDirectory
    }
    else {
        if (-Not (Split-Path -IsAbsolute -Path $OutputDirectory)) {
            $OutputDirectory = Join-Path -Path $PSScriptRoot -ChildPath $OutputDirectory
        }

        # Resolving the absolute path to save the required modules to
        if (-Not (Split-Path -IsAbsolute -Path $RequiredModulesDirectory)) {
            $RequiredModulesDirectory = Join-Path -Path $PSScriptRoot -ChildPath $RequiredModulesDirectory
        }

        # Create the output/modules folder if not exists, or resolve the Absolute path otherwise
        if (Resolve-Path $RequiredModulesDirectory -ErrorAction SilentlyContinue) {
            Write-Debug "[pre-build] Required Modules path already exist at $RequiredModulesDirectory"
            $RequiredModulesPath = Convert-Path $RequiredModulesDirectory
        }
        else {
            Write-Host -foregroundColor Green "[pre-build] Creating required modules directory $RequiredModulesDirectory."
            $RequiredModulesPath = (New-Item -ItemType Directory -Force -Path $RequiredModulesDirectory).FullName
        }

        # Prepending $RequiredModulesPath folder to PSModulePath to resolve from this folder FIRST
        if ($RequiredModulesDirectory -notIn @('CurrentUser', 'AllUsers') -and
            (($Env:PSModulePath -split [io.path]::PathSeparator) -notContains $RequiredModulesDirectory)) {
            Write-Host -foregroundColor Green "[pre-build] Prepending '$RequiredModulesDirectory' folder to PSModulePath"
            $Env:PSModulePath = $RequiredModulesDirectory + [io.path]::PathSeparator + $Env:PSModulePath
        }

        # Prepending $OutputDirectory folder to PSModulePath to resolve built module from this folder
        if (($Env:PSModulePath -split [io.path]::PathSeparator) -notContains $OutputDirectory) {
            Write-Host -foregroundColor Green "[pre-build] Prepending '$OutputDirectory' folder to PSModulePath"
            $Env:PSModulePath = $OutputDirectory + [io.path]::PathSeparator + $Env:PSModulePath
        }

        # Tell Resolve-Dependency to use $RequiredModulesPath as -PSDependTarget if not overridden in Build.psd1
        $PSDependTarget = $RequiredModulesPath
    }

    if ($ResolveDependency) {
        Write-Host -Object "[pre-build] Resolving dependencies." -foregroundColor Green
        $ResolveDependencyParams = @{ }

        # If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency
        if ($BuildConfig -match '\.[yaml|yml]$') {
            $ResolveDependencyParams.add('WithYaml', $True)
        }

        # TODO: 3 way merge: BoundParameter over BuildInfo over Scope variables (i.e. defaults from args)
        $ResolveDependencyAvailableParams = (get-command -Name '.\Resolve-Dependency.ps1').parameters.keys
        foreach ($CmdParameter in $ResolveDependencyAvailableParams) {

            # The parameter has been explicitly used for calling the .build.ps1
            if ($MyInvocation.BoundParameters.ContainsKey($CmdParameter)) {
                $ParamValue = $MyInvocation.BoundParameters.ContainsKey($CmdParameter)
                Write-Debug " adding $CmdParameter :: $ParamValue [from user-provided parameters to Build.ps1]"
                $ResolveDependencyParams.Add($CmdParameter, $ParamValue)
            }
            # Use defaults parameter value from Build.ps1, if any
            else {
                if ($ParamValue = Get-Variable -Name $CmdParameter -ValueOnly -ErrorAction Ignore) {
                    Write-Debug " adding $CmdParameter :: $ParamValue [from default Build.ps1 variable]"
                    $ResolveDependencyParams.add($CmdParameter, $ParamValue)
                }
            }
        }

        Write-Host -foregroundColor Green "[pre-build] Starting bootstrap process."
        .\Resolve-Dependency.ps1 @ResolveDependencyParams
    }

    if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') {
        Write-Verbose "Bootstrap completed. Handing back to InvokeBuild."
        if ($PSBoundParameters.ContainsKey('ResolveDependency')) {
            Write-Verbose "Dependency already resolved. Removing task"
            $null = $PSBoundParameters.Remove('ResolveDependency')
        }
        Write-Host -foregroundColor Green "[build] Starting build with InvokeBuild."
        Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path
        Pop-Location -StackName BuildModule
        return
    }
}