Templates/Build/build.ps1
<#
.DESCRIPTION Bootstrap and build script for PowerShell module pipeline #> [CmdletBinding()] param ( [Parameter(Position = 0)] [string[]]$Tasks = '.', [Parameter()] [String] $CodeCoverageThreshold = '', [Parameter()] [validateScript( { Test-Path -Path $_ } )] $BuildConfig, [Parameter()] # A Specific folder to build the artefact into. $OutputDirectory = 'output', [Parameter()] # Subdirectory name to build the module (under $OutputDirectory) $BuiltModuleSubdirectory = '', # 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'), [Parameter()] [object[]] $PesterScript, # Filter which tags to run when invoking Pester tests # This is used in the Invoke-Pester.pester.build.ps1 tasks [Parameter()] [string[]] $PesterTag, # Filter which tags to exclude when invoking Pester tests # This is used in the Invoke-Pester.pester.build.ps1 tasks [Parameter()] [string[]] $PesterExcludeTag, # Filter which tags to run when invoking DSC Resource tests # This is used in the DscResource.Test.build.ps1 tasks [Parameter()] [string[]] $DscTestTag, # Filter which tags to exclude when invoking DSC Resource tests # This is used in the DscResource.Test.build.ps1 tasks [Parameter()] [string[]] $DscTestExcludeTag, [Parameter()] [Alias('bootstrap')] [switch]$ResolveDependency, [Parameter(DontShow)] [AllowNull()] $BuildInfo, [Parameter()] [switch] $AutoRestore ) # 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' { if (-not (Get-Command -Name Import-PowerShellDataFile -ErrorAction SilentlyContinue)) { Import-Module Microsoft.PowerShell.Utility -RequiredVersion 3.1.0.0 } 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)/\*.*?\*/' ConvertFrom-Yaml -Yaml $JSON # Yaml is superset of 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 Config $ConfigFile.`r`n Are you missing dependencies?" -ForegroundColor Yellow Write-Host -Object "Make sure you run './build.ps1 -ResolveDependency -tasks noop' to restore the Required modules the first time" -ForegroundColor Yellow $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 { Write-Host -ForegroundColor DarkGray -Verbose "Importing tasks from module $Module" $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 Write-Host -ForegroundColor DarkGray "Adding Workflow from configuration:" 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 " +-> $Workflow" task $Workflow $WorkflowItem } Write-Host -ForeGroundColor magenta "[build] Executing requested workflow: $($Tasks -join ', ')" } finally { Pop-Location -StackName BeforeBuild } } Begin { # Find build config if not specified if (-not $BuildConfig) { $config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction:Ignore if (-not $config -or ($config -is [array] -and $config.Length -le 0)) { throw "No build configuration found. Specify path via -BuildConfig" } elseif ($config -is [array]) { if ($config.Length -gt 1) { throw "More than one build configuration found. Specify which one to use via -BuildConfig" } $BuildConfig = $config[0] } else { $BuildConfig = $config } } # 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 } # Checking if the user should -ResolveDependency if ((!(Get-Module -ListAvailable powershell-yaml) -or !(Get-Module -ListAvailable InvokeBuild) -or !(Get-Module -ListAvailable PSDepend)) -and !$ResolveDependency) { if ($AutoRestore -or !$PSBoundParameters.ContainsKey('Tasks') -or $Tasks -contains 'build') { Write-Host -ForegroundColor Yellow "[pre-build] Dependency missing, running './build.ps1 -ResolveDependency -Tasks noop' for you `r`n" $ResolveDependency = $true } else { Write-Warning "Some required Modules are missing, make sure you first run with the '-ResolveDependency' parameter." Write-Warning "Running 'build.ps1 -ResolveDependency -Tasks noop' will pull required modules without running the build task." } } if ($BuiltModuleSubdirectory) { if (-Not (Split-Path -IsAbsolute $BuiltModuleSubdirectory)) { $BuildModuleOutput = Join-Path $OutputDirectory $BuiltModuleSubdirectory } else { $BuildModuleOutput = $BuiltModuleSubdirectory } } else { $BuildModuleOutput = $OutputDirectory } # Prepending $BuildModuleOutput folder to PSModulePath to resolve built module from this folder if (($Env:PSModulePath -split [io.path]::PathSeparator) -notContains $BuildModuleOutput) { Write-Host -foregroundColor Green "[pre-build] Prepending '$BuildModuleOutput' folder to PSModulePath" $Env:PSModulePath = $BuildModuleOutput + [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) } $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 } } |