Builder.psm1
#Requires -Version 2.0 ####################################################################### # Localization data ####################################################################### # Ignore error if localization for current UICulture is unavailable Import-LocalizedData -BindingVariable PBLocalizedData -BaseDirectory $PSScriptRoot -FileName 'Message.psd1' -ErrorAction $( if ($PSVersionTable.PSVersion.Major -ge 3) { 'Ignore' } else { 'SilentlyContinue' } ) # Fallback to US English if localization data failed to load # Do not continue if fallback failed to load too if (-not $PBLocalizedData) { Import-LocalizedData -BindingVariable PBLocalizedData -BaseDirectory $PSScriptRoot -UICulture 'en-US' -FileName 'Message.psd1' -ErrorVariable loadDefaultLocalizationError -ErrorAction $( if ($PSVersionTable.PSVersion.Major -ge 3) { 'Ignore' } else { 'SilentlyContinue' } ) # Continue with error if localization variable is available # Otherwise stop if ($loadDefaultLocalizationError) { if (-not $PBLocalizedData) { $PSCmdlet.ThrowTerminatingError($loadDefaultLocalizationError[0]) } else { $loadDefaultLocalizationError[0] } } } # This shouldn't happen. Just in case. if (-not $PBLocalizedData) { if (-not (Test-Path (Join-Path $PSScriptRoot -ChildPath 'en-US/Message.psd1') -PathType Leaf)) { # This will generate the ItemNotFound exception Get-Content (Join-Path $PSScriptRoot -ChildPath 'en-US/Message.psd1') -ErrorVariable localizationFileNotFoundError -ErrorAction $( if ($PSVersionTable.PSVersion.Major -ge 3) { 'Ignore' } else { 'SilentlyContinue' } ) $localizationException = $localizationFileNotFoundError[0].Exception if (-not $localizationException) { # This shouldn't happen, but just in case $localizationException = "Cannot find path '{0}' because it does not exist." -f (Join-Path $PSScriptRoot -ChildPath 'en-US/Message.psd1') } $PSCmdlet.ThrowTerminatingError(( New-Object 'System.Management.Automation.ErrorRecord' -ArgumentList $localizationException, 'DefaultLocalizationFileNotFound', 'ObjectNotFound', $null )) } else { $localizationError = New-Object 'System.Management.Automation.ErrorRecord' -ArgumentList ("An error has occured while loading the '{0}' localization data file." -f (Join-Path $PSScriptRoot -ChildPath 'en-US/Message.psd1')), 'InvalidLocalizationFile', 'InvalidData', $null $PSCmdlet.ThrowTerminatingError($localizationError) } } ####################################################################### # Public module functions ####################################################################### function Exec { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$Command, [Parameter(Mandatory = $false)] [string]$ErrorMessage = ($PBLocalizedData.Err_BadCommand -f $Command), [Parameter(Mandatory = $false)] [int]$MaxRetry = 0, [Parameter(Mandatory = $false)] [ValidateRange(1, [Int]::MaxValue)] [int]$RetryDelay = 1, [Parameter(Mandatory = $false)] [string]$RetryTriggerErrorPattern = $null, [Parameter(Mandatory = $false)] [switch]$NoWill ) $tryCount = 1 do { try { $global:LASTEXITCODE = 0 & $Command if ($LASTEXITCODE -ne 0) { Die $ErrorMessage 'ExecError' -NoWill:$NoWill } break } catch [Exception] { if ($tryCount -gt $MaxRetry) { Die $_ 'ExecError' -NoWill:$NoWill } if ($RetryTriggerErrorPattern -ne $null) { $isMatch = [RegEx]::IsMatch($_.Exception.Message, $RetryTriggerErrorPattern) if ($isMatch -eq $false) { Die $_ 'ExecError' -NoWill:$NoWill } } Write-Output ("[EXEC] " + ($PBLocalizedData.RetryMessage -f $tryCount, $MaxRetry, $RetryDelay)) $tryCount++ Start-Sleep -Seconds $RetryDelay } } while ($true) } function Assert { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] $Condition, [Parameter(Position = 2, Mandatory = $true)] $ErrorMessage, [Parameter()] [switch]$NoWill ) if (-not $Condition) { Die $ErrorMessage 'AssertConditionFailure' -NoWill:$NoWill } } function Properties { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$ScriptBlock ) $BuildEnv.Context.Peek().Properties += $ScriptBlock } function Will { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$ScriptBlock ) $BuildEnv.Context.Peek().Will += $ScriptBlock } function PrintTask { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] $Format ) $BuildEnv.Context.Peek().Setting.TaskNameFormat = $Format } function Include { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [string]$FilePath ) Assert (Test-Path $FilePath -PathType Leaf) -ErrorMessage ($PBLocalizedData.Err_InvalidIncludePath -f $FilePath) $BuildEnv.Context.Peek().Includes.Enqueue((Resolve-Path $FilePath)) } function TaskSetup { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$ScriptBlock ) $BuildEnv.Context.Peek().TaskSetupScriptBlock = $ScriptBlock } function TaskTearDown { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$ScriptBlock ) $BuildEnv.Context.Peek().TaskTearDownScriptBlock = $ScriptBlock } function EnvPath { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [string[]]$Path ) $BuildEnv.Context.Peek().Setting.EnvPath = $Path ConfigureBuildEnvironment } function Invoke-Task { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 1)] [string]$TaskName ) Assert $TaskName ($PBLocalizedData.Err_InvalidTaskName) $taskKey = $TaskName.ToLower() if ($CurrentContext.Aliases.Contains($taskKey)) { $TaskName = $CurrentContext.Aliases."$taskKey".Name $taskKey = $taskName.ToLower() } $CurrentContext = $BuildEnv.Context.Peek() Assert ($CurrentContext.Tasks.Contains($taskKey)) -ErrorMessage ($PBLocalizedData.Err_TaskNameDoesNotExist -f $TaskName) if ($CurrentContext.ExecutedTasks.Contains($taskKey)) { return } Assert (-not $CurrentContext.CallStack.Contains($taskKey)) -ErrorMessage ($PBLocalizedData.Err_CircularReference -f $TaskName) $CurrentContext.CallStack.Push($taskKey) $task = $CurrentContext.Tasks.$taskKey $preconditionIsValid = & $task.Precondition if (-not $preconditionIsValid) { WriteColoredOutput ($PBLocalizedData.PreconditionWasFalse -f $TaskName) -ForegroundColor Cyan } else { if ($taskKey -ne 'default') { if ($task.PreAction -or $task.PostAction) { Assert ($task.Action -ne $null) -ErrorMessage ($PBLocalizedData.Err_MissingActionParameter -f $TaskName) } if ($task.Action) { try { foreach ($childTask in $task.DependsOn) { Invoke-Task $childTask } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $CurrentContext.CurrentTaskName = $TaskName & $CurrentContext.TaskSetupScriptBlock if ($task.PreAction) { & $task.PreAction } if ($CurrentContext.Setting.TaskNameFormat -is [scriptblock]) { & $currentContext.Setting.TaskNameFormat $TaskName } else { WriteColoredOutput ($CurrentContext.Setting.TaskNameFormat -f $TaskName) -ForegroundColor Cyan } foreach ($reqVar in $task.RequiredVariables) { Assert ((Test-Path "Variable:$reqVar") -and ((Get-Variable $reqVar).Value -ne $null)) -ErrorMessage ($PBLocalizedData.RequiredVarNotSet -f $reqVar, $TaskName) } & $task.Action if ($task.PostAction) { & $task.PostAction } & $CurrentContext.TaskTearDownScriptBlock $task.Duration = $stopwatch.Elapsed } catch { if ($task.ContinueOnError) { Write-Output $PBLocalizedData.Divider WriteColoredOutput ($PBLocalizedData.ContinueOnError -f $TaskName, $_) -ForegroundColor Yellow Write-Output $PBLocalizedData.Divider $task.Duration = $stopwatch.Elapsed } else { Die $_ 'InvokeTaskError' -NoWill } } } else { # no action was specified but we still execute all the dependencies foreach ($childTask in $task.DependsOn) { Invoke-Task $childTask } } } else { foreach ($childTask in $task.DependsOn) { Invoke-Task $childTask } } Assert (& $task.PostCondition) -ErrorMessage ($PBLocalizedData.PostconditionFailed -f $TaskName) } $poppedTaskKey = $CurrentContext.CallStack.Pop() Assert ($poppedTaskKey -eq $taskKey) -ErrorMessage ($PBLocalizedData.Err_CorruptCallStack -f $taskKey, $poppedTaskKey) $CurrentContext.ExecutedTasks.Push($taskKey) } function Task { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [string]$Name, [Parameter(Position = 2, Mandatory = $false)] [scriptblock]$Action, [Parameter(Mandatory = $false)] [scriptblock]$PreAction, [Parameter(Mandatory = $false)] [scriptblock]$PostAction, [Parameter(Mandatory = $false)] [scriptblock]$Precondition = { $true }, [Parameter(Mandatory = $false)] [scriptblock]$Postcondition = { $true }, [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [string[]]$Depends = @(), [Parameter(Mandatory = $false)] [string[]]$RequiredVariables = @(), [Parameter(Mandatory = $false)] [string]$Description, [Parameter(Mandatory = $false)] [string]$Alias ) if ($Name -eq 'default') { Assert (-not $Action) -ErrorMessage ($PBLocalizedData.Err_DefaultTaskCannotHaveAction) } $newTask = @{ Name = $Name DependsOn = $Depends PreAction = $PreAction Action = $Action PostAction = $PostAction Precondition = $Precondition Postcondition = $Postcondition ContinueOnError = $ContinueOnError Description = $Description Duration = [System.TimeSpan]::Zero RequiredVariables = $RequiredVariables Alias = $Alias } $taskKey = $Name.ToLower() $CurrentContext = $BuildEnv.Context.Peek() Assert (-not $CurrentContext.Tasks.ContainsKey($taskKey)) -ErrorMessage ($PBLocalizedData.Err_DuplicateTaskName -f $Name) $CurrentContext.Tasks.$taskKey = $newTask if ($Alias) { $aliasKey = $Alias.ToLower() Assert (-not $CurrentContext.Aliases.ContainsKey($aliasKey)) -ErrorMessage ($PBLocalizedData.Err_DuplicateAliasName -f $Alias) $CurrentContext.Aliases.$aliasKey = $newTask } } function Say { # .EXTERNALHELP Builder-Help.xml [CmdletBinding(DefaultParameterSetName = 'NormalSet')] Param( [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'NormalSet')] [string]$Message, [Parameter(Mandatory = $true, ParameterSetName = 'DividerSet')] [switch]$Divider, [Parameter(Mandatory = $true, ParameterSetName = 'NewLineSet')] [switch]$NewLine, [Parameter(Mandatory = $false, ParameterSetName = 'NewLineSet')] [ValidateRange(1, [Int]::MaxValue)] [int]$LineCount = 1, [Parameter(Mandatory = $false, ParameterSetName = 'NormalSet')] [ValidateRange(0, 6)] [Alias('v')] [int]$VerboseLevel = 1, [Parameter(Mandatory = $false, ParameterSetName = 'NormalSet')] [Alias('fg')] [System.ConsoleColor]$ForegroundColor = 'Yellow', [Parameter(Mandatory = $false)] [switch]$Force ) # configured verbose level = 0 --> no output except errors if ((-not $Force) -and ($BuildEnv.Context.Peek().Setting.VerboseLevel -eq 0)) { return } # this works even if $Host is not around $dividerMaxLength = [Math]::Max(70, $Host.UI.RawUI.WindowSize.Width - 1) if ($PSCmdlet.ParameterSetName -eq 'DividerSet') { Write-Output ([Environment]::NewLine) WriteColoredOutput ('+' * $dividerMaxLength) -ForegroundColor Cyan Write-Output ([Environment]::NewLine) } elseif ($PSCmdlet.ParameterSetName -eq 'NewLineSet') { Write-Output ([Environment]::NewLine * $LineCount) } elseif ($PSCmdlet.ParameterSetName -eq 'NormalSet') { # suppress output if verbose level > configured verbose level if ((-not $Force) -and ($VerboseLevel -gt $BuildEnv.Context.Peek().Setting.VerboseLevel)) { return } WriteColoredOutput $Message -ForegroundColor $( if ($VerboseLevel -eq 0) { 'Red' } elseif ($VerboseLevel -eq 1) { $ForegroundColor } elseif ($VerboseLevel -eq 2) { 'Green' } elseif ($VerboseLevel -eq 3) { 'Magenta' } elseif ($VerboseLevel -eq 4) { 'DarkMagenta' } else { 'Gray' } ) } } function Die { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 1)] [AllowEmptyString()] [AllowNull()] [string]$Message, [Parameter(Mandatory = $false, Position = 2)] [string]$ErrorCode = 'BuildError', [Parameter(Mandatory = $false)] [switch]$NoWill ) if ($NoWill) { # Do no execute wills (if any) and die instantly } else { #$currentContext = $BuildEnv.Context.Peek() $currentTaskName = $CurrentContext.CallStack.Peek() if ($CurrentContext.Will) { foreach ($willBlock in $CurrentContext.Will) { . $willBlock $currentTaskName } } } if ($Message -eq '') { $Message = $PBLocalizedData.UnknownError } $errRecord = New-Object 'System.Management.Automation.ErrorRecord' -ArgumentList $Message, $ErrorCode, 'InvalidOperation', $null $PSCmdlet.ThrowTerminatingError($errRecord) } function Get-BuildScriptTasks { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $false)] [string]$BuildFile ) if (-not $BuildFile) { $BuildFile = $BuildEnv.DefaultSetting.BuildFileName } try { ExecuteInBuildFileScope { Param($CurrentContext, $Module) return GetTasksFromContext $CurrentContext } -BuildFile $BuildFile -Module ($MyInvocation.MyCommand.Module) } finally { CleanupEnvironment } } function Invoke-Builder { # .EXTERNALHELP Builder-Help.xml [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $false)] [string]$BuildFile, [Parameter(Position = 2, Mandatory = $false)] [string[]]$TaskList = @(), [Parameter(Mandatory = $false)] [switch]$Docs, [Parameter(Mandatory = $false)] [hashtable]$Parameters = @{}, [Parameter(Mandatory = $false)] $Properties = @{}, [Parameter(Mandatory = $false)] [Alias('Init')] [scriptblock]$Initialization = {}, [Parameter(Mandatory = $false)] [switch]$NoLogo, [Parameter(Mandatory = $false)] [switch]$DetailDocs, [Parameter(Mandatory = $false)] [switch]$TimeReport ) try { if (-not $NoLogo) { $logoText = @( ('Builder {0}' -f $BuildEnv.Version) 'Copyright (c) 2018 Lizoc Inc. All rights reserved.' '' ) -join [Environment]::NewLine Write-Output $logoText } if (-not $BuildFile) { $BuildFile = $BuildEnv.DefaultSetting.BuildFileName } elseif (-not (Test-Path $BuildFile -PathType Leaf) -and (Test-Path $BuildEnv.DefaultSetting.BuildFileName -PathType Leaf)) { # if the $config.buildFileName file exists and the given "buildfile" isn 't found assume that the given # $buildFile is actually the target Tasks to execute in the $config.buildFileName script. $taskList = $BuildFile.Split(', ') $BuildFile = $BuildEnv.DefaultSetting.BuildFileName } ExecuteInBuildFileScope -BuildFile $BuildFile -Module ($MyInvocation.MyCommand.Module) -ScriptBlock { Param($CurrentContext, $Module) $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() if ($Docs -or $DetailDocs) { WriteDocumentation -Detail:$DetailDocs return } foreach ($key in $Parameters.Keys) { if (Test-Path "Variable:\$key") { Set-Item -Path "Variable:\$key" -Value $Parameters.$key -WhatIf:$false -Confirm:$false | Out-Null } else { New-Item -Path "Variable:\$key" -Value $Parameters.$key -WhatIf:$false -Confirm:$false | Out-Null } } # The initial dot (.) indicates that variables initialized/modified in the propertyBlock are available in the parent scope. foreach ($propertyBlock in $CurrentContext.Properties) { . $propertyBlock } foreach ($key in $Properties.Keys) { if (Test-Path "Variable:\$key") { Set-Item -Path "Variable:\$key" -Value $Properties.$key -WhatIf:$false -Confirm:$false | Out-Null } } # Simple dot sourcing will not work. We have to force the script block into our # module's scope in order to initialize variables properly. . $Module $Initialization # Execute the list of tasks or the default task if ($taskList) { foreach ($task in $taskList) { Invoke-Task $task } } elseif ($CurrentContext.Tasks.Default) { Invoke-Task default } else { Die $PBLocalizedData.Err_NoDefaultTask 'NoDefaultTask' } $outputMessage = @( '' $PBLocalizedData.BuildSuccess '' ) -join [Environment]::NewLine WriteColoredOutput $outputMessage -ForegroundColor Green $stopwatch.Stop() if ($TimeReport) { WriteTaskTimeSummary $stopwatch.Elapsed } } $BuildEnv.BuildSuccess = $true } catch { $currentConfig = GetCurrentConfigurationOrDefault if ($currentConfig.VerboseError) { $errMessage = @( ('[{0}] {1}' -f (Get-Date).ToString('hhmm:ss'), $PBLocalizedData.ErrorHeaderText) '' ('{0}: {1}' -f $PBLocalizedData.ErrorLabel, (ResolveError $_ -Short)) $PBLocalizedData.Divider (ResolveError $_) # this will have enough blank lines appended $PBLocalizedData.Divider $PBLocalizedData.VariableLabel $PBLocalizedData.Divider (Get-Variable -Scope Script | Format-Table | Out-String) ) -join [Environment]::NewLine } else { # ($_ | Out-String) gets error messages with source information included. $errMessage = '[{0}] {1}: {2}' -f (Get-Date).ToString('hhmm:ss'), $PBLocalizedData.ErrorLabel, (ResolveError $_ -Short) } $BuildEnv.BuildSuccess = $false # if we are running in a nested scope (i.e. running a build script from within another build script) then we need to re-throw the exception # so that the parent script will fail otherwise the parent script will report a successful build $inNestedScope = ($BuildEnv.Context.Count -gt 1) if ($inNestedScope) { Die $_ } else { if (-not $BuildEnv.RunByUnitTest) { WriteColoredOutput $errMessage -ForegroundColor Red } } } finally { CleanupEnvironment } } ####################################################################### # Private module functions ####################################################################### function WriteColoredOutput { [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 1)] [string]$Message, [Parameter(Mandatory = $true, Position = 2)] [System.ConsoleColor]$ForegroundColor ) $currentConfig = GetCurrentConfigurationOrDefault if ($currentConfig.ColoredOutput -eq $true) { if (($Host.UI -ne $null) -and ($Host.UI.RawUI -ne $null) -and ($Host.UI.RawUI.ForegroundColor -ne $null)) { $previousColor = $Host.UI.RawUI.ForegroundColor $Host.UI.RawUI.ForegroundColor = $ForegroundColor } } Write-Output $message if ($previousColor -ne $null) { $Host.UI.RawUI.ForegroundColor = $previousColor } } function ExecuteInBuildFileScope { [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] [scriptblock]$ScriptBlock, [Parameter(Mandatory = $true)] [string]$BuildFile, [Parameter(Mandatory = $true)] $Module ) # Execute the build file to set up the tasks and defaults Assert (Test-Path $BuildFile -PathType Leaf) -ErrorMessage ($PBLocalizedData.Err_BuildFileNotFound -f $BuildFile) $BuildEnv.BuildScriptFile = Get-Item $BuildFile $BuildEnv.BuildScriptDir = $BuildEnv.BuildScriptFile.DirectoryName $BuildEnv.BuildSuccess = $false $BuildEnv.Context.Push(@{ 'TaskSetupScriptBlock' = {} 'TaskTearDownScriptBlock' = {} 'ExecutedTasks' = New-Object System.Collections.Stack 'CallStack' = New-Object System.Collections.Stack 'OriginalEnvPath' = $env:Path 'OriginalDirectory' = Get-Location 'OriginalErrorActionPreference' = $global:ErrorActionPreference 'Tasks' = @{} 'Aliases' = @{} 'Properties' = @() 'Will' = @() 'Includes' = New-Object System.Collections.Queue 'Setting' = CreateConfigurationForNewContext -BuildFile $BuildFile }) LoadConfiguration $BuildEnv.BuildScriptDir Set-Location $BuildEnv.BuildScriptDir LoadModules . $BuildEnv.BuildScriptFile.FullName $CurrentContext = $BuildEnv.Context.Peek() ConfigureBuildEnvironment while ($CurrentContext.Includes.Count -gt 0) { $includeFilename = $CurrentContext.Includes.Dequeue() . $includeFilename } & $ScriptBlock $CurrentContext $Module } function WriteDocumentation { [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [switch]$Detail ) $currentContext = $BuildEnv.Context.Peek() if ($currentContext.Tasks.Default) { $defaultTaskDependencies = $currentContext.Tasks.Default.DependsOn } else { $defaultTaskDependencies = @() } $docs = GetTasksFromContext $currentContext | where { $_.Name -ne 'default' } | ForEach-Object { $isDefault = $null if ($defaultTaskDependencies -contains $_.Name) { $isDefault = $true } Add-Member -InputObject $_ 'Default' $isDefault -Passthru } if ($Detail) { $docs | sort 'Name' | Format-List -Property Name, Alias, Description, @{ Label = 'Depends On' Expression = { $_.DependsOn -join ', '} }, Default } else { $docs | sort 'Name' | Format-Table -AutoSize -Wrap -Property Name, Alias, @{ Label = 'Depends On' Expression = { $_.DependsOn -join ', ' } }, Default, Description } } function ResolveError { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] $ErrorRecord = $Error[0], [Parameter(Mandatory = $false)] [switch]$Short ) Process { if ($_ -eq $null) { $_ = $ErrorRecord } $ex = $_.Exception if (-not $Short) { $errMessage = @( '' 'ErrorRecord:{0}ErrorRecord.InvocationInfo:{1}Exception:' '{2}' '' ) -join [Environment]::NewLine $formattedErrRecord = $_ | Format-List * -Force | Out-String $formattedInvocationInfo = $_.InvocationInfo | Format-List * -Force | Out-String $formattedException = '' $i = 0 while ($ex -ne $null) { $i++ $formattedException += @( ("$i" * 70) ($ex | Format-List * -Force | Out-String) '' ) -join [Environment]::NewLine $ex = $ex | SelectObjectWithDefault -Name 'InnerException' -Value $null } return $errMessage -f $formattedErrRecord, $formattedInvocationInfo, $formattedException } $lastException = @() while ($ex -ne $null) { $lastMessage = $ex | SelectObjectWithDefault -Name 'Message' -Value '' $lastException += ($lastMessage -replace [Environment]::NewLine, '') if ($ex -is [Data.SqlClient.SqlException]) { $lastException = '(Line [{0}] Procedure [{1}] Class [{2}] Number [{3}] State [{4}])' -f $ex.LineNumber, $ex.Procedure, $ex.Class, $ex.Number, $ex.State } $ex = $ex | SelectObjectWithDefault -Name 'InnerException' -Value $null } $shortException = $lastException -join ' --> ' $header = $null $current = $_ $header = (($_.InvocationInfo | SelectObjectWithDefault -Name 'PositionMessage' -Value '') -replace [Environment]::NewLine, ' '), ($_ | SelectObjectWithDefault -Name 'Message' -Value ''), ($_ | SelectObjectWithDefault -Name 'Exception' -Value '') | where { -not [String]::IsNullOrEmpty($_) } | select -First 1 $delimiter = '' if ((-not [String]::IsNullOrEmpty($header)) -and (-not [String]::IsNullOrEmpty($shortException))) { $delimiter = ' [<<==>>] ' } return '{0}{1}Exception: {2}' -f $header, $delimiter, $shortException } } function LoadModules { $currentConfig = $BuildEnv.Context.Peek().Setting if ($currentConfig.Modules) { $scope = $currentConfig.ModuleScope $global = [string]::Equals($scope, 'global', [StringComparison]::CurrentCultureIgnoreCase) $currentConfig.Modules | ForEach-Object { Resolve-Path $_ | ForEach-Object { # "Loading module: $_" $module = Import-Module $_ -PassThru -DisableNameChecking -Global:$global -Force if (-not $module) { Die ($PBLocalizedData.Err_LoadingModule -f $_.Name) 'LoadModuleError' } } } Write-Output '' } } function LoadConfiguration { [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [string]$ConfigPath = $PSScriptRoot ) $pbConfigFilePath = Join-Path $ConfigPath -ChildPath "Builder-Config.ps1" if (Test-Path $pbConfigFilePath -PathType Leaf) { try { $config = GetCurrentConfigurationOrDefault . $pbConfigFilePath } catch { Die ($PBLocalizedData.Err_LoadConfig + ': ' + $_) 'LoadConfigError' } } } function GetCurrentConfigurationOrDefault() { if ($BuildEnv.Context.Count -gt 0) { $BuildEnv.Context.Peek().Setting } else { $BuildEnv.DefaultSetting } } function CreateConfigurationForNewContext { [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [string]$BuildFile ) $previousConfig = GetCurrentConfigurationOrDefault $config = New-Object PSObject -Property @{ BuildFileName = $previousConfig.BuildFileName EnvPath = $previousConfig.EnvPath TaskNameFormat = $previousConfig.TaskNameFormat VerboseError = $previousConfig.VerboseError ColoredOutput = $previousConfig.ColoredOutput Modules = $previousConfig.Modules ModuleScope = $previousConfig.ModuleScope VerboseLevel = $previousConfig.VerboseLevel } if ($BuildFile) { $config.BuildFileName = $BuildFile } $config } function ConfigureBuildEnvironment { $envPathDirs = @($BuildEnv.Context.Peek().Setting.EnvPath) | where { ($_ -ne $null) -and ($_ -ne '') } if ($envPathDirs) { $envPathDirs | ForEach-Object { Assert (Test-Path $_ -PathType Container) -ErrorMessage ($PBLocalizedData.Err_EnvPathDirNotFound -f $_) } $newEnvPath = @($env:Path.Split([System.IO.Path]::PathSeparator), $envPathDirs) | select -Unique $env:Path = $newEnvPath -join [System.IO.Path]::PathSeparator } # if any error occurs in a PS function then "stop" processing immediately # this does not effect any external programs that return a non-zero exit code $global:ErrorActionPreference = 'Stop' } function CleanupEnvironment { if ($BuildEnv.Context.Count -gt 0) { $currentContext = $BuildEnv.Context.Peek() $env:Path = $currentContext.OriginalEnvPath Set-Location $currentContext.OriginalDirectory $global:ErrorActionPreference = $currentContext.OriginalErrorActionPreference [void]$BuildEnv.Context.Pop() } } function SelectObjectWithDefault { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [psobject]$InputObject, [Parameter(ValueFromPipeline = $false)] [string]$Name, [Parameter(ValueFromPipeline = $false)] $Value ) Process { if ($_ -eq $null) { $Value } elseif ($_ | Get-Member -Name $Name) { $_."$Name" } elseif (($_ -is [Hashtable]) -and ($_.Keys -contains $Name)) { $_."$Name" } else { $Value } } } function GetTasksFromContext { [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] $CurrentContext ) $CurrentContext.Tasks.Keys | ForEach-Object { $task = $CurrentContext.Tasks."$_" New-Object PSObject -Property @{ Name = $task.Name Alias = $task.Alias Description = $task.Description DependsOn = $task.DependsOn } } } function WriteTaskTimeSummary { [CmdletBinding()] Param( [Parameter(Position = 1, Mandatory = $true)] $Duration ) if ($BuildEnv.Context.Count -gt 0) { Write-Output $PBLocalizedData.Divider Write-Output $PBLocalizedData.BuildTimeReportTitle Write-Output $PBLocalizedData.Divider $list = @() $currentContext = $BuildEnv.Context.Peek() while ($currentContext.ExecutedTasks.Count -gt 0) { $taskKey = $currentContext.ExecutedTasks.Pop() $task = $currentContext.Tasks.$taskKey if ($taskKey -eq 'default') { continue } $list += New-Object PSObject -Property @{ Name = $task.Name Duration = $task.Duration } } [Array]::Reverse($list) $list += New-Object PSObject -Property @{ Name = 'Total' Duration = $Duration } # using "out-string | where-object" to filter out the blank line that format-table prepends $list | Format-Table -AutoSize -Property Name, Duration | Out-String -Stream | where { $_ } } } ####################################################################### # Main ####################################################################### $scriptDir = Split-Path $MyInvocation.MyCommand.Path $manifestPath = Join-Path $scriptDir -ChildPath 'Builder.psd1' $manifest = Test-ModuleManifest -Path $manifestPath -WarningAction $( if ($PSVersionTable.PSVersion.Major -ge 3) { 'Ignore' } else { 'SilentlyContinue' } ) $script:BuildEnv = @{} $BuildEnv.Version = $manifest.Version.ToString() $BuildEnv.Context = New-Object System.Collections.Stack # holds onto the current state of all variables $BuildEnv.RunByUnitTest = $false # indicates that build is being run by internal unit tester # contains default configuration, can be overriden in Builder-Config.ps1 in directory with Builder.psm1 or in directory with current build script $BuildEnv.DefaultSetting = New-Object PSObject -Property @{ BuildFileName = 'default.ps1' EnvPath = $null TaskNameFormat = $PBLocalizedData.DefaultTaskNameFormat VerboseError = $false ColoredOutput = $true Modules = $null ModuleScope = '' VerboseLevel = 2 } $BuildEnv.BuildSuccess = $false # indicates that the current build was successful $BuildEnv.BuildScriptFile = $null # contains a System.IO.FileInfo for the current build script $BuildEnv.BuildScriptDir = '' # contains a string with fully-qualified path to current build script $BuildEnv.ModulePath = $PSScriptRoot LoadConfiguration Export-ModuleMember -Function @( 'Invoke-Builder', 'Invoke-Task', 'Get-BuildScriptTasks', 'Task', 'PrintTask', 'TaskSetup', 'TaskTearDown', 'Properties', 'Include', 'Will', 'EnvPath', 'Assert', 'Exec', 'Say', 'Die' ) -Variable @( 'BuildEnv' ) |