Invoke-Build.ps1

<# Invoke-Build 5.12.0
Copyright (c) Roman Kuzmin

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
#>


#.ExternalHelp Help.xml
param(
    [Parameter(Position=0)][string[]]$Task,
    [Parameter(Position=1)]$File,
    $Result,
    [switch]$Safe,
    [switch]$Summary,
    [switch]$WhatIf
)

dynamicparam {

function *Path($P) {
    $PSCmdlet.GetUnresolvedProviderPathFromPSPath($P)
}

function *Die($M, $C=0) {
    $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]"$M"), $null, $C, $null))
}

function Get-BuildFile($Path) {
    do {
        if (($f = [System.IO.Directory]::GetFiles($Path, '*.build.ps1')).Length -eq 1) {return $f}
        if ($f) {return $($f | Sort-Object)[0]}
        if (($c = $env:InvokeBuildGetFile) -and ($f = & $c $Path)) {return $f}
    } while($Path = Split-Path $Path)
}

if ($MyInvocation.InvocationName -eq '.') {return}
trap {*Die $_ 5}

$p = if ($_ = $PSCmdlet.SessionState.PSVariable.Get('*')) {if ($_.Description -eq 'IB') {$_.Value}}
$c, $r, $a = $null
New-Variable * -Description IB ([PSCustomObject]@{
    All = [System.Collections.Specialized.OrderedDictionary]([System.StringComparer]::OrdinalIgnoreCase)
    Tasks = [System.Collections.Generic.List[object]]@()
    Errors = [System.Collections.Generic.List[object]]@()
    Warnings = [System.Collections.Generic.List[object]]@()
    Redefined = @()
    Doubles = @()
    Started = [DateTime]::Now
    Elapsed = $null
    Error = 'Invalid arguments.'
    Task = $null
    File = $BuildFile = $PSBoundParameters['File']
    Safe = $PSBoundParameters['Safe']
    Summary = $PSBoundParameters['Summary']
    CD = $OriginalLocation = *Path
    DP = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    SP = @{}
    P = $p
    A = 1
    B = 0
    Q = 0
    H = @{}
    EnterBuild = $null
    ExitBuild = $null
    EnterTask = $null
    ExitTask = $null
    EnterJob = $null
    ExitJob = $null
    Header = if ($p) {$p.Header} else {{Write-Build 11 "Task $($args[0])"}}
    Footer = if ($p) {$p.Footer} else {{Write-Build 11 "Done $($args[0]) $($Task.Elapsed)"}}
    Data = @{}
    XBuild = $null
    XCheck = $null
})
if ($_ = $PSBoundParameters['Result']) {
    if ($_ -is [string]) {
        New-Variable $_ ${*} -Scope 1 -Force
    }
    elseif ($_ -is [hashtable]) {
        ${*}.XBuild = $_['XBuild']
        ${*}.XCheck = $_['XCheck']
        $_.Value = ${*}
    }
    else {throw 'Invalid parameter Result.'}
}
$BuildTask = $PSBoundParameters['Task']
if ($BuildFile -is [scriptblock]) {
    $BuildFile = $BuildFile.File
    return
}
if ($BuildTask -eq '**') {
    if (![System.IO.Directory]::Exists(($_ = *Path $BuildFile))) {throw "Missing directory '$_'."}
    $BuildFile = @(Get-ChildItem -LiteralPath $_ -Filter *.test.ps1 -Recurse -Force)
    return
}

if ($BuildFile) {
    if (![System.IO.File]::Exists(($BuildFile = *Path $BuildFile))) {throw "Missing script '$BuildFile'."}
}
elseif (!($BuildFile = Get-BuildFile ${*}.CD)) {
    throw 'Missing default script.'
}
${*}.File = $BuildFile

if (!($_ = (Get-Command $BuildFile -ErrorAction 1).Parameters)) {
    & $BuildFile
    throw 'Invalid script.'
}
if ($_.get_Count()) {
    $c = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'ErrorVariable', 'WarningVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'InformationAction', 'InformationVariable', 'ProgressAction'
    $r = 'Task', 'File', 'Result', 'Safe', 'Summary', 'WhatIf'
    foreach($p in $_.get_Values()) {
        if ($c -contains ($_ = $p.Name)) {continue}
        if ($r -contains $_) {throw "Script uses reserved parameter '$_'."}
        foreach ($a in $p.Attributes) {
            if ($a -is [System.Management.Automation.ParameterAttribute] -and $a.Position -ge 0) {
                $a.Position += 2
            }
        }
        ${*}.DP.Add($_, (New-Object System.Management.Automation.RuntimeDefinedParameter $_, $p.ParameterType, $p.Attributes))
    }
    ${*}.DP
}

} end {

#.ExternalHelp Help.xml
function Add-BuildTask(
    [Parameter(Position=0, Mandatory=1)][string]$Name,
    [Parameter(Position=1)]$Jobs,
    [string[]]$After,
    [string[]]$Before,
    $If=-9,
    $Inputs,
    $Outputs,
    $Data,
    $Done,
    $Source=$MyInvocation,
    [switch]$Partial
)
{
    trap {*Die "Task '$Name': $_" 5}
    if (${*}.A -eq 0) {throw 'Cannot add tasks.'}
    if ($Jobs -is [hashtable]) {
        if ($PSBoundParameters.get_Count() -ne 2) {throw 'Invalid parameters.'}
        Add-BuildTask $Name @Jobs -Source:$Source
        return
    }
    if ($Name[0] -eq '?') {throw 'Invalid task name.'}
    if ($_ = ${*}.All[$Name]) {${*}.Redefined += $_}
    ${*}.All[$Name] = [PSCustomObject]@{
        Name = $Name
        Error = $null
        Started = $null
        Elapsed = $null
        Jobs = $1 = [System.Collections.Generic.List[object]]@()
        After = $After
        Before = $Before
        If = $If
        Inputs = $Inputs
        Outputs = $Outputs
        Data = $Data
        Done = $Done
        Partial = $Partial
        InvocationInfo = $Source
    }
    if (!$Jobs) {return}
    $1.AddRange(@($Jobs))
    $2 = @()
    foreach($j in $1) {
        $r, $null = *Job $j
        if ($2 -contains $r) {${*}.Doubles += ,($Name, $r)}
        $2 += $r
    }
}

#.ExternalHelp Help.xml
function Assert-Build([Parameter()]$Condition, [string]$Message) {
    if (!$Condition) {
        *Die "Assertion failed.$(if ($Message) {" $Message"})" 7
    }
}

#.ExternalHelp Help.xml
function Assert-BuildEquals([Parameter()]$A, $B) {
    if (![Object]::Equals($A, $B)) {
        *Die @"
Objects are not equal:
A:$(if ($null -ne $A) {" $A [$($A.GetType())]"})
B:$(if ($null -ne $B) {" $B [$($B.GetType())]"})
"@
 7
    }
}

#.ExternalHelp Help.xml
function Get-BuildError([Parameter(Mandatory=1)][string]$Task) {
    if (!($_ = ${*}.All[$Task])) {
        *Die "Missing task '$Task'." 5
    }
    $_.Error
}

#.ExternalHelp Help.xml
function Get-BuildProperty([Parameter(Mandatory=1)][string]$Name, $Value) {
    ${*n} = $Name
    ${*v} = $Value
    Remove-Variable Name, Value
    if (($null -ne ($_ = $PSCmdlet.GetVariableValue(${*n})) -and '' -ne $_) -or ($_ = [Environment]::GetEnvironmentVariable(${*n}))) {return $_}
    if ($null -eq ${*v}) {*Die "Missing property '${*n}'." 13}
    ${*v}
}

#.ExternalHelp Help.xml
function Get-BuildSynopsis([Parameter(Mandatory=1)]$Task, $Hash=${*}.H) {
    $f = ($I = $Task.InvocationInfo).ScriptName
    if (!($d = $Hash[$f])) {
        $Hash[$f] = $d = @{T = Get-Content -LiteralPath $f; C = @{}}
        foreach($_ in [System.Management.Automation.PSParser]::Tokenize($d.T, [ref]$null)) {
            if ($_.Type -eq 15) {$d.C[$_.EndLine] = $_.Content}
        }
    }
    for($n = $I.ScriptLineNumber; --$n -ge 1) {
        if ($c = $d.C[$n]) {if ($c -match '(?m)^\s*(?:#*\s*Synopsis\s*:|\.Synopsis\s*^)(.*)') {return $Matches[1].Trim()}}
        elseif ($d.T[$n - 1].Trim()) {break}
    }
}

#.ExternalHelp Help.xml
function Use-BuildEnv([Parameter(Mandatory=1)][hashtable]$Env, [Parameter(Mandatory=1)][scriptblock]$Script) {
    ${private:*e} = @{}
    ${private:*s} = $Script
    function *set($n, $v) {
        [Environment]::SetEnvironmentVariable($n, $(if ($null -eq $v) {[System.Management.Automation.Language.NullString]::Value} else {$v}))
    }
    foreach($_ in $Env.GetEnumerator()) {
        ${*e}[$_.Key] = [Environment]::GetEnvironmentVariable($_.Key)
        *set $_.Key $_.Value
    }
    Remove-Variable Env, Script
    try {
        & ${*s}
    }
    finally {
        foreach($_ in ${*e}.GetEnumerator()) {
            *set $_.Key $_.Value
        }
    }
}

#.ExternalHelp Help.xml
function Invoke-BuildExec([Parameter(Mandatory=1)][scriptblock]$Command, [int[]]$ExitCode=0, [string]$ErrorMessage, [switch]$Echo, [switch]$StdErr) {
    ${private:*c} = $Command
    ${private:*x} = $ExitCode
    ${private:*m} = $ErrorMessage
    ${private:*v} = $Echo
    ${private:*s} = $StdErr
    ${private:*e} = ''
    Remove-Variable Command, ExitCode, ErrorMessage, Echo, StdErr
    if (${*v}) {
        *Echo ${*c}
    }

    $global:LastExitCode = 0
    if (${*s}) {
        $ErrorActionPreference = 2
        try {
            & ${*c} 2>&1 | .{process{
                if ($_ -is [System.Management.Automation.ErrorRecord]) {
                    $_ = $_.Exception.Message
                    ${*e} += "`n$_"
                }
                $_
            }}
        }
        catch {throw}
    }
    else {
        & ${*c}
    }

    if (${*x} -notcontains $global:LastExitCode) {
        *Die "$(if (${*m}) {"${*m} "})Command exited with code $global:LastExitCode. {${*c}}${*e}" 8
    }
}

function *Echo {
    ${*c} = $args[0]
    ${*t} = "${*c}".Replace("`t", ' ')
    Write-Build 3 "exec {$(if (${*t} -match '((?:\r\n|[\r\n]) *)\S') {"$(${*t}.TrimEnd().Replace($matches[1], "`n "))`n"} else {${*t}})}"
    Write-Build 8 "cd $global:pwd"
    foreach(${*v} in ${*c}.Ast.FindAll({$args[0] -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)) {
        ${*p} = ${*v}.Parent
        if (${*p} -is [System.Management.Automation.Language.MemberExpressionAst]) {
            if (${*p} -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) {continue}
            ${*v} = ${*p}
        }
        if (${*v}.Parent -isnot [System.Management.Automation.Language.AssignmentStatementAst]) {
            ${*t} = "${*v}" -replace '^@', '$'
            Write-Build 8 "${*t}: $(& ([scriptblock]::Create(${*t})))"
        }
    }
}

#.ExternalHelp Help.xml
function Remove-BuildItem([Parameter(Mandatory=1)][string[]]$Path) {
    if ($Path -match '^[.*/\\]*$') {*Die 'Not allowed paths.' 5}
    $v = $PSBoundParameters['Verbose']
    try {
        foreach($_ in $Path) {
            if (Get-Item $_ -Force -ErrorAction 0) {
                if ($v) {Write-Verbose "remove: removing $_" -Verbose}
                Remove-Item $_ -Force -Recurse
            }
            elseif ($v) {Write-Verbose "remove: skipping $_" -Verbose}
        }
    }
    catch {
        *Die $_
    }
}

#.ExternalHelp Help.xml
function Test-BuildAsset(
    [ValidateNotNull()][string[]][Parameter(Position=0)]$Variable,
    [ValidateNotNull()][string[]]$Environment,
    [ValidateNotNull()][string[]]$Property,
    [ValidateNotNull()][string[]]$Path
) {
    Remove-Variable Variable, Environment, Property, Path
    function *get($p, $n) {
        if ($_ = $p[$n]) {
            $_ | .{process{if ($_) {$_} else {*Die "Invalid empty '$n'."}}}
        }
    }
    foreach($_ in *get $PSBoundParameters Variable) {
        if ($null -eq ($$ = $PSCmdlet.GetVariableValue($_)) -or '' -eq $$) {*Die "Missing variable '$_'." 13}
    }
    foreach($_ in *get $PSBoundParameters Environment) {
        if (!([Environment]::GetEnvironmentVariable($_))) {*Die "Missing environment variable '$_'." 13}
    }
    foreach($_ in *get $PSBoundParameters Property) {
        if ('' -eq (Get-BuildProperty $_ '')) {*Die "Missing property '$_'." 13}
    }
    foreach($_ in *get $PSBoundParameters Path) {
        if (!(Test-Path -LiteralPath $_)) {*Die "Missing path '$_'." 13}
    }
}

#.ExternalHelp Help.xml
function Use-BuildAlias([Parameter(Mandatory=1)][string]$Path, [string[]]$Name) {
    trap {*Die $_ 5}
    $d = switch -regex ($Path) {
        '^\*|^\d+\.' {Split-Path (Resolve-MSBuild $_)}
        ^Framework {"$env:windir\Microsoft.NET\$_"}
        default {*Path $_}
    }
    if (![System.IO.Directory]::Exists($d)) {throw "Cannot resolve '$Path'."}
    foreach($_ in $Name) {
        Set-Alias $_ (Join-Path $d $_) -Scope 1
    }
}

#.ExternalHelp Help.xml
function Set-BuildFooter([Parameter()][scriptblock]$Script) {${*}.Footer = $Script}

#.ExternalHelp Help.xml
function Set-BuildHeader([Parameter()][scriptblock]$Script) {${*}.Header = $Script}

#.ExternalHelp Help.xml
function Confirm-Build([Parameter()][string]$Query, [string]$Caption=$Task.Name) {
    $PSCmdlet.ShouldContinue($Query, $Caption)
}

#.ExternalHelp Help.xml
function Write-Build([ConsoleColor]$Color, [string]$Text) {
    *Write $Color ($Text -split '\r\n|[\r\n]')
}

if ($PSVersionTable.PSVersion -ge [Version]'7.2' -and $PSStyle.OutputRendering -ne 'PlainText') {
    function *Write($C, $T) {
        $f = "`e[$((30,34,32,36,31,35,33,37,90,94,92,96,91,95,93,97)[$C])m{0}`e[0m"
        foreach($_ in $T) {
            $f -f $_
        }
    }
}
else {
    function *Write($C, $T) {
        $i = $Host.UI.RawUI
        $_ = $i.ForegroundColor
        try {
            $i.ForegroundColor = $C
            $T
        }
        finally {
            $i.ForegroundColor = $_
        }
    }
    try {
        $null = *Write 0
    }
    catch {
        function *Write {$args[1]}
    }
}

function *My {
    $_.InvocationInfo.ScriptName -eq $MyInvocation.ScriptName
}

function *SL($P=$BuildRoot) {
    Set-Location -LiteralPath $P -ErrorAction 1
}

function *Fin([Parameter()]$M, $C=0) {
    *Die $M $C
}

function *Run($_) {
    if ($_) {
        *SL
        . $_ @args
    }
}

function *At($I) {
    $I.InvocationInfo.PositionMessage.Trim()
}

function *Msg($M, $I) {
    "$M`n$(*At $I)"
}

function *Job($J) {
    if ($J -is [string]) {if ($J[0] -eq '?') {$J.Substring(1), 1} else {$J}}
    elseif ($J -is [scriptblock]) {$J}
    else {*Fin 'Invalid job.' 5}
}

function *Unsafe($N, $J) {
    if ($J -contains $N) {return 1}
    foreach($_ in $J) {
        $r, $null = *Job $_
        if ($r -ne $N -and ($t = ${*}.All[$r]) -and $t.If -and (*Unsafe $N $t.Jobs)) {
            return 1
        }
    }
}

function *Amend($X, $J, $B) {
    $n = $X.Name
    foreach($_ in $J) {
        $r, $s = *Job $_
        if (!($t = ${*}.All[$r])) {*Fin (*Msg "Task '$n': Missing task '$r'." $X) 5}
        $j = $t.Jobs
        $i = $j.Count
        if ($B) {
            for($k = -1; ++$k -lt $i -and $j[$k] -is [string]) {}
            $i = $k
        }
        $j.Insert($i, $(if ($s) {"?$n"} else {$n}))
    }
}

function *Check($J, $T, $P=@()) {
    foreach($_ in $J) { if ($_ -is [string]) {
        $_, $null = *Job $_
        if (!($r = ${*}.All[$_])) {
            $_ = "Missing task '$_'."
            *Fin $(if ($T) {*Msg "Task '$($T.Name)': $_" $T} else {$_}) 5
        }
        if ($P -contains $r) {
            *Fin (*Msg "Task '$($T.Name)': Cyclic reference to '$_'." $T) 5
        }
        *Check $r.Jobs $r ($P + $r)
    }}
}

filter *Help {
    $r = 1 | Select-Object Name, Jobs, Synopsis
    $r.Name = $_.Name
    $r.Jobs = foreach($j in $_.Jobs) {if ($j -is [string]) {$j} else {'{}'}}
    $r.Synopsis = Get-BuildSynopsis $_
    $r
}

function *Root($A) {
    *Check $A.get_Keys()
    $h = @{}
    foreach($_ in $A.get_Values()) {foreach($_ in $_.Jobs) {
        if ($_ -is [string]) {
            $_, $null = *Job $_
            $h[$_] = 1
        }
    }}
    foreach($_ in $A.get_Keys()) {if (!$h[$_]) {$_}}
}

function *Err($T) {
    ${*}.Errors.Add([PSCustomObject]@{Error = $_; File = $BuildFile; Task = $T})
    Write-Build 12 "ERROR: $(if (*My) {$_} else {*Msg $_ $_})"
    if ($T) {$T.Error = $_}
}

function *IO {
    if ((${private:*i} = $Task.Inputs) -is [scriptblock]) {
        *SL
        ${*i} = @(& ${*i})
    }
    *SL
    ${private:*p} = [System.Collections.Generic.List[object]]@()
    ${*i} = foreach($_ in ${*i}) {
        if ($_ -isnot [System.IO.FileInfo]) {$_ = [System.IO.FileInfo](*Path $_)}
        if (!$_.Exists) {*Fin "Missing input '$_'." 13}
        ${*p}.Add($_.FullName)
        $_
    }
    if (!${*p}) {return 2, 'Skipping empty input.'}

    ${private:*o} = $Task.Outputs
    if ($Task.Partial) {
        ${*o} = @(
            if (${*o} -is [scriptblock]) {
                ${*p} | & ${*o}
                *SL
            }
            else {
                ${*o}
            }
        )
        if (${*p}.Count -ne ${*o}.Count) {*Fin "Different Inputs/Outputs counts: $(${*p}.Count)/$(${*o}.Count)." 6}

        $k = -1
        $Task.Inputs = $i = [System.Collections.Generic.List[object]]@()
        $Task.Outputs = $o = [System.Collections.Generic.List[object]]@()
        foreach($_ in ${*i}) {
            $f = *Path ($p = ${*o}[++$k])
            if (![System.IO.File]::Exists($f) -or $_.LastWriteTime -gt [System.IO.File]::GetLastWriteTime($f)) {
                $i.Add(${*p}[$k])
                $o.Add($p)
            }
        }
        if ($i) {return $null, "Out-of-date outputs: $($o.Count)/$(${*p}.Count)."}
    }
    else {
        if (${*o} -is [scriptblock]) {
            $Task.Outputs = ${*o} = ${*p} | & ${*o}
            *SL
        }
        if (!${*o}) {*Fin 'Outputs must not be empty.' 5}

        $Task.Inputs = ${*p}
        $m = (${*i} | .{process{$_.LastWriteTime.Ticks}} | Measure-Object -Maximum).Maximum
        foreach($_ in ${*o}) {
            $p = *Path $_
            if (![System.IO.File]::Exists($p)) {return $null, "Missing output '$_'."}
            if ($m -gt [System.IO.File]::GetLastWriteTime($p).Ticks) {return $null, "Out-of-date output '$_'."}
        }
    }
    2, 'Skipping up-to-date output.'
}

function *Task {
    ${private:*p} = "$($args[1])/$($args[0])"
    ${private:*n}, ${private:*s} = *Job $args[0]
    New-Variable Task (${*}.Task = ${*}.All[${*n}]) -Option Constant

    if ($Task.Elapsed) {
        Write-Build 8 "Done ${*p}"
        return
    }

    $Task.Started = [DateTime]::Now
    if ((${private:*x} = $Task.If) -is [scriptblock]) {
        *SL
        try {
            ${*x} = & ${*x}
        }
        catch {
            *Err $Task
            Write-Build 8 (*At $Task)
            ${*}.Tasks.Add($Task)
            $Task.Elapsed = [TimeSpan]::Zero
            throw
        }
    }
    if (!${*x}) {
        Write-Build 8 "Task ${*p} skipped."
        return
    }

    ${private:*i} = , [int]($null -ne $Task.Inputs)
    try {
        . *Run ${*}.EnterTask
        foreach($_ in $Task.Jobs) {
            if ($_ -is [string]) {
                try {
                    *Task $_ ${*p}
                }
                finally {
                    ${*}.Task = $Task
                }
                continue
            }
            New-Variable Job $_ -Option ReadOnly -Force
            & ${*}.Header ${*p}

            if (1 -eq ${*i}[0]) {
                try {
                    ${*i} = *IO
                }
                catch {
                    *Err $Task
                    throw
                }
                Write-Build 8 ${*i}[1]
            }
            if (${*i}[0]) {
                continue
            }

            try {
                . *Run ${*}.EnterJob
                *SL
                if (0 -eq ${*i}[0]) {
                    & $Job
                }
                else {
                    $Inputs = $Task.Inputs
                    $Outputs = $Task.Outputs
                    if ($Task.Partial) {
                        ${*x} = 0
                        $Inputs | .{process{
                            $2 = $Outputs[${*x}++]
                            $_
                        }} | & $Job
                    }
                    else {
                        $Inputs | & $Job
                    }
                }
            }
            catch {
                *Err $Task
                Write-Build 8 (*At $Task)
                throw
            }
            finally {
                . *Run ${*}.ExitJob
            }
        }
    }
    catch {
        $Task.Error = $_
        if (!${*s} -or (*Unsafe ${*n} $BuildTask)) {throw}
    }
    finally {
        $Task.Elapsed = [DateTime]::Now - $Task.Started
        ${*}.Tasks.Add($Task)
        if (!$Task.Error) {
            if (${*}.XCheck) {& ${*}.XCheck}
            & ${*}.Footer ${*p}
        }
        *Run $Task.Done
        . *Run ${*}.ExitTask
    }
}

Set-Alias assert Assert-Build
Set-Alias equals Assert-BuildEquals
Set-Alias exec Invoke-BuildExec
Set-Alias property Get-BuildProperty
Set-Alias remove Remove-BuildItem
Set-Alias requires Test-BuildAsset
Set-Alias task Add-BuildTask
Set-Alias use Use-BuildAlias
Set-Alias Invoke-Build ($_ = $MyInvocation.MyCommand.Path)
$_ = Split-Path $_
Set-Alias Show-TaskHelp (Join-Path $_ Show-TaskHelp.ps1)
Set-Alias Build-Parallel (Join-Path $_ Build-Parallel.ps1)
Set-Alias Resolve-MSBuild (Join-Path $_ Resolve-MSBuild.ps1)

if ($MyInvocation.InvocationName -eq '.') {
    Remove-Variable Task, File, Result, Safe, Summary, WhatIf
    return
}

function Write-Warning([Parameter()]$Message) {
    $PSCmdlet.WriteWarning($Message)
    ${*}.Warnings.Add([PSCustomObject]@{Message = $Message; File = $BuildFile; Task = ${*}.Task; InvocationInfo=$MyInvocation})
}

$ErrorActionPreference = 1
foreach($_ in ${*}.DP.get_Values()) {
    if ($_.IsSet) {
        ${*}.SP[$_.Name] = $_.Value
    }
}
if (${*}.Q = $BuildTask -eq '?' -or $BuildTask -eq '??') {
    $WhatIf = $true
}
Remove-Variable Task, File, Result, Safe, Summary, p, c, r, a

${*}.Error = $null
try {
    if ($BuildTask -eq '**') {
        ${*}.A = 0
        foreach($_ in $BuildFile) {
            Invoke-Build * $_.FullName -Safe:${*}.Safe
        }
        ${*}.B = 1
        exit
    }

    function Enter-Build([Parameter()][scriptblock]$Script) {${*}.EnterBuild = $Script}
    function Exit-Build([Parameter()][scriptblock]$Script) {${*}.ExitBuild = $Script}
    function Enter-BuildTask([Parameter()][scriptblock]$Script) {${*}.EnterTask = $Script}
    function Exit-BuildTask([Parameter()][scriptblock]$Script) {${*}.ExitTask = $Script}
    function Enter-BuildJob([Parameter()][scriptblock]$Script) {${*}.EnterJob = $Script}
    function Exit-BuildJob([Parameter()][scriptblock]$Script) {${*}.ExitJob = $Script}
    function Set-BuildData([Parameter()]$Key, $Value) {${*}.Data[$Key] = $Value}

    *SL ($BuildRoot = if ($BuildFile) {Split-Path $BuildFile} else {${*}.CD})
    New-Variable Task @{Name = $BuildFile} -Option Constant
    $_ = ${*}.SP
    ${private:**} = @(. ${*}.File @_)
    foreach($_ in ${**}) {
        Write-Warning "Unexpected output: $_."
        if ($_ -is [scriptblock]) {*Fin "Dangling scriptblock at $($_.File):$($_.StartPosition.StartLine)" 6}
    }
    if (!(${**} = ${*}.All).get_Count()) {*Fin "No tasks in '$BuildFile'." 6}

    foreach($_ in ${**}.get_Values()) {
        if ($_.Before) {*Amend $_ $_.Before 1}
    }
    foreach($_ in ${**}.get_Values()) {
        if ($_.After) {*Amend $_ $_.After}
    }

    if (${*}.Q) {
        *Check ${**}.get_Keys()
        if ($BuildTask -eq '?') {
            ${**}.get_Values() | *Help
        }
        else {
            ${**}
        }
        exit
    }

    if ($BuildTask -eq '*') {
        $BuildTask = *Root ${**}
    }
    else {
        if (!$BuildTask -or '.' -eq $BuildTask) {
            $BuildTask = if (${**}['.']) {'.'} else {${**}.Item(0).Name}
        }
        *Check $BuildTask
    }
    if ($WhatIf) {
        Show-TaskHelp
        exit
    }

    New-Variable BuildRoot (*Path $BuildRoot) -Option Constant -Force
    if (![System.IO.Directory]::Exists($BuildRoot)) {*Fin "Missing build root '$BuildRoot'." 13}

    Write-Build 11 "Build $($BuildTask -join ', ') $BuildFile"
    foreach($_ in ${*}.Redefined) {
        Write-Build 8 "Redefined task '$($_.Name)'."
    }
    foreach($_ in ${*}.Doubles) {
        if (${*}.All[$_[1]].If -isnot [scriptblock]) {
            Write-Warning "Task '$($_[0])' always skips '$($_[1])'."
        }
    }

    ${*}.A = 0
    try {
        . *Run ${*}.EnterBuild
        if (${*}.XBuild) {. ${*}.XBuild}
        if (${*}.XCheck) {& ${*}.XCheck}
        foreach($_ in $BuildTask) {
            *Task $_ ''
        }
    }
    finally {
        ${*}.Task = $null
        . *Run ${*}.ExitBuild
    }
    ${*}.B = 1
    exit
}
catch {
    ${*}.B = 2
    ${*}.Error = $_
    if (!${*}.Errors) {*Err}
    if ($_.FullyQualifiedErrorId -eq 'PositionalParameterNotFound,Add-BuildTask') {
        Write-Warning 'Check task parameters: Name and comma separated Jobs.'
    }
    if (${*}.Safe) {
        exit
    }
    elseif (*My) {
        $PSCmdlet.ThrowTerminatingError($_)
    }
    throw
}
finally {
    *SL ${*}.CD
    if (${*}.B -and !${*}.Q) {
        $t = ${*}.Tasks
        $e = ${*}.Errors
        if (${*}.Summary) {
            Write-Build 11 'Build summary:'
            foreach($_ in $t) {
                '{0,-16} {1} - {2}:{3}' -f $_.Elapsed, $_.Name, $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber
                if ($_ = $_.Error) {
                    Write-Build 12 "ERROR: $(if (*My) {$_} else {*Msg $_ $_})"
                }
            }
        }
        if ($w = ${*}.Warnings) {
            foreach($_ in $w) {
                "WARNING: $(if ($_.Task) {"/$($_.Task.Name) "})$($_.InvocationInfo.ScriptName):$($_.InvocationInfo.ScriptLineNumber)"
                Write-Build 14 $_.Message
            }
        }
        if ($_ = ${*}.P) {
            $_.Tasks.AddRange($t)
            $_.Errors.AddRange($e)
            $_.Warnings.AddRange($w)
        }
        $c, $m = if (${*}.A) {12, "Build ABORTED $BuildFile"}
        elseif (${*}.B -eq 2) {12, 'Build FAILED'}
        elseif ($e) {14, 'Build completed with errors'}
        elseif ($w) {14, 'Build succeeded with warnings'}
        else {10, 'Build succeeded'}
        Write-Build $c "$m. $($t.Count) tasks, $($e.Count) errors, $($w.Count) warnings $((${*}.Elapsed = [DateTime]::Now - ${*}.Started))"
    }
}
}