diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8293224 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +[Bb]in/ +[Oo]bj/ +[Ww]orking*/ +Build/runbuild.txt +Build/nuget.exe +Doc/doc.shfbproj_* +TestResults/ +AppPackages/ +*.suo +*.user +*.userprefs +_ReSharper.* +*.ReSharper.user +*.resharper.user +.vs/ +*.lock.json +*.nuget.props +*.nuget.targets +*.orig +.DS_Store diff --git a/build/build.ps1 b/build/build.ps1 new file mode 100644 index 0000000..4f8ff93 --- /dev/null +++ b/build/build.ps1 @@ -0,0 +1,236 @@ +properties { + + $majorVersion = "1.0" + $majorWithReleaseVersion = "1.0.10" + $version = Get-Version $majorWithReleaseVersion + $baseDir = resolve-path .. + $sourceDir = "$baseDir\Src" + $signAssemblies = $false + $packageId = "SqlSharpener" + $treatWarningsAsErrors = $false + $buildNuGet = $true + $workingName = if ($workingName) {$workingName} else {"Working"} + $workingDir = "$baseDir\$workingName" + $workingSourceDir = "$workingDir\Src" + $builds = @( + @{Name = "SqlSharpener"; TestsName = "SqlSharpener.Tests"; BuildFunction = "MSBuildBuild"; TestsFunction = "NUnitTests"; Constants="NET40"; FinalDir="net40"; NuGetDir = "net40"; Framework="net-4.0"} + ) +} + +task default -depends Test + +# Ensure a clean working directory +task Clean { + Write-Host "Setting location to $baseDir" + Set-Location $baseDir + + if (Test-Path -path $workingDir) + { + Write-Host "Deleting existing working directory $workingDir" + + Execute-Command -command { del $workingDir -Recurse -Force } + } + + Write-Host "Creating working directory $workingDir" + New-Item -Path $workingDir -ItemType Directory +} + +# Build each solution, optionally signed +task Build -depends Clean { + Write-Host "Copying source to working source directory $workingSourceDir" + robocopy $sourceDir $workingSourceDir /MIR /NP /XD bin obj TestResults AppPackages $packageDirs .vs artifacts /XF *.suo *.user *.lock.json | Out-Default + + Write-Host -ForegroundColor Green "Updating assembly version" + Write-Host + Update-AssemblyInfoFiles $workingSourceDir ($majorVersion + '.0.0') $version + + foreach ($build in $builds) + { + $name = $build.Name + if ($name -ne $null) + { + Write-Host -ForegroundColor Green "Building " $name + + & $build.BuildFunction $build + } + } +} + +# Optional build documentation, add files to final zip +task Package -depends Build { + foreach ($build in $builds) + { + $name = $build.TestsName + $finalDir = $build.FinalDir + + robocopy "$workingSourceDir\SqlSharpener\bin\Release\$finalDir" $workingDir\Package\Bin\$finalDir *.dll *.pdb *.xml /NFL /NDL /NJS /NC /NS /NP /XO /XF *.CodeAnalysisLog.xml | Out-Default + } + + if ($buildNuGet) + { + $nugetVersion = $majorWithReleaseVersion + if ($nugetPrelease -ne $null) + { + $nugetVersion = $nugetVersion + "-" + $nugetPrelease + } + + New-Item -Path $workingDir\NuGet -ItemType Directory + + $nuspecPath = "$workingDir\NuGet\SqlSharpener.nuspec" + Copy-Item -Path "$baseDir\dist\SqlSharpener.nuspec" -Destination $nuspecPath -recurse + + Write-Host "Updating nuspec file at $nuspecPath" -ForegroundColor Green + Write-Host + + $xml = [xml](Get-Content $nuspecPath) + Edit-XmlNodes -doc $xml -xpath "//*[local-name() = 'id']" -value $packageId + Edit-XmlNodes -doc $xml -xpath "//*[local-name() = 'version']" -value $nugetVersion + + Write-Host $xml.OuterXml + + $xml.save($nuspecPath) + + foreach ($build in $builds) + { + if ($build.NuGetDir) + { + $name = $build.TestsName + $finalDir = $build.FinalDir + $frameworkDirs = $build.NuGetDir.Split(",") + + foreach ($frameworkDir in $frameworkDirs) + { + robocopy "$workingSourceDir\SqlSharpener\bin\Release\$finalDir" $workingDir\NuGet\tools *.dll *.pdb *.xml /NFL /NDL /NJS /NC /NS /NP /XO /XF *.CodeAnalysisLog.xml | Out-Default + } + } + } + + #robocopy $workingSourceDir $workingDir\NuGet\src *.cs /S /NFL /NDL /NJS /NC /NS /NP /XD Newtonsoft.Json.Tests Newtonsoft.Json.TestConsole obj .vs artifacts | Out-Default + + Write-Host "Building NuGet package with ID $packageId and version $nugetVersion" -ForegroundColor Green + Write-Host + + exec { .\Tools\NuGet\NuGet.exe pack $nuspecPath -Symbols } + move -Path .\*.nupkg -Destination $workingDir\NuGet + } +} + +# Unzip package to a location +task Deploy -depends Package { +} + + +# Run tests on deployed files +task Test -depends Deploy { +} + +function MSBuildBuild($build) +{ + $name = $build.Name + $finalDir = $build.FinalDir + + Write-Host + Write-Host "Restoring $workingSourceDir\$name.sln" -ForegroundColor Green + [Environment]::SetEnvironmentVariable("EnableNuGetPackageRestore", "true", "Process") + exec { .\Tools\NuGet\NuGet.exe update -self } + exec { .\Tools\NuGet\NuGet.exe restore "$workingSourceDir\$name.sln" -verbosity detailed | Out-Default } "Error restoring $name" + + $constants = Get-Constants $build.Constants $signAssemblies + + Write-Host + Write-Host "Building $workingSourceDir\$name.sln" -ForegroundColor Green + exec { msbuild "/t:Clean;Rebuild" /p:Configuration=Release "/p:CopyNuGetImplementations=true" "/p:Platform=Any CPU" "/p:PlatformTarget=AnyCPU" /p:OutputPath=bin\Release\$finalDir\ "/p:SignAssembly=$signAssemblies" "/p:TreatWarningsAsErrors=$treatWarningsAsErrors" "/p:VisualStudioVersion=14.0" /p:DefineConstants=`"$constants`" "$workingSourceDir\$name.sln" | Out-Default } "Error building $name" +} + +function Get-Constants($constants, $includeSigned) +{ + $signed = switch($includeSigned) { $true { ";SIGNED" } default { "" } } + + return "CODE_ANALYSIS;TRACE;$constants$signed" +} + +function Get-Version($majorVersion) +{ + $now = [DateTime]::Now + + $year = $now.Year - 2000 + $month = $now.Month + $totalMonthsSince2000 = ($year * 12) + $month + $day = $now.Day + $minor = "{0}{1:00}" -f $totalMonthsSince2000, $day + + $hour = $now.Hour + $minute = $now.Minute + $revision = "{0:00}{1:00}" -f $hour, $minute + + return $majorVersion + "." + $minor +} + +function Update-AssemblyInfoFiles ([string] $workingSourceDir, [string] $assemblyVersionNumber, [string] $fileVersionNumber) +{ + $assemblyVersionPattern = 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)' + $fileVersionPattern = 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)' + $assemblyVersion = 'AssemblyVersion("' + $assemblyVersionNumber + '")'; + $fileVersion = 'AssemblyFileVersion("' + $fileVersionNumber + '")'; + + Get-ChildItem -Path $workingSourceDir -r -filter AssemblyInfo.cs | ForEach-Object { + + $filename = $_.Directory.ToString() + '\' + $_.Name + Write-Host $filename + $filename + ' -> ' + $version + + (Get-Content $filename) | ForEach-Object { + % {$_ -replace $assemblyVersionPattern, $assemblyVersion } | + % {$_ -replace $fileVersionPattern, $fileVersion } + } | Set-Content $filename + } +} + + +function Edit-XmlNodes { + param ( + [xml] $doc, + [string] $xpath = $(throw "xpath is a required parameter"), + [string] $value = $(throw "value is a required parameter") + ) + + $nodes = $doc.SelectNodes($xpath) + $count = $nodes.Count + + Write-Host "Found $count nodes with path '$xpath'" + + foreach ($node in $nodes) { + if ($node -ne $null) { + if ($node.NodeType -eq "Element") + { + $node.InnerXml = $value + } + else + { + $node.Value = $value + } + } + } +} + +function Execute-Command($command) { + $currentRetry = 0 + $success = $false + do { + try + { + & $command + $success = $true + } + catch [System.Exception] + { + if ($currentRetry -gt 5) { + throw $_.Exception.ToString() + } else { + write-host "Retry $currentRetry" + Start-Sleep -s 1 + } + $currentRetry = $currentRetry + 1 + } + } while (!$success) +} diff --git a/build/runbuild.cmd b/build/runbuild.cmd new file mode 100644 index 0000000..a440787 --- /dev/null +++ b/build/runbuild.cmd @@ -0,0 +1,5 @@ +cls +powershell -Command "& { [Console]::WindowWidth = 150; [Console]::WindowHeight = 50; Start-Transcript %~dp0runbuild.txt; Import-Module %~dp0..\Tools\PSake\psake.psm1; Invoke-psake %~dp0..\Build\build.ps1 %*; Stop-Transcript; exit !($psake.build_success); }" + +ECHO %ERRORLEVEL% +EXIT /B %ERRORLEVEL% diff --git a/tools/NuGet/NuGet.exe b/tools/NuGet/NuGet.exe new file mode 100644 index 0000000..03ddcca Binary files /dev/null and b/tools/NuGet/NuGet.exe differ diff --git a/tools/PSake/psake.ps1 b/tools/PSake/psake.ps1 new file mode 100644 index 0000000..23c6f2d --- /dev/null +++ b/tools/PSake/psake.ps1 @@ -0,0 +1,53 @@ +# Helper script for those who want to run psake without importing the module. +# Example run from PowerShell: +# .\psake.ps1 "default.ps1" "BuildHelloWord" "4.0" + +# Must match parameter definitions for psake.psm1/invoke-psake +# otherwise named parameter binding fails +param( + [Parameter(Position=0,Mandatory=0)] + [string]$buildFile, + [Parameter(Position=1,Mandatory=0)] + [string[]]$taskList = @(), + [Parameter(Position=2,Mandatory=0)] + [string]$framework, + [Parameter(Position=3,Mandatory=0)] + [switch]$docs = $false, + [Parameter(Position=4,Mandatory=0)] + [System.Collections.Hashtable]$parameters = @{}, + [Parameter(Position=5, Mandatory=0)] + [System.Collections.Hashtable]$properties = @{}, + [Parameter(Position=6, Mandatory=0)] + [alias("init")] + [scriptblock]$initialization = {}, + [Parameter(Position=7, Mandatory=0)] + [switch]$nologo = $false, + [Parameter(Position=8, Mandatory=0)] + [switch]$help = $false, + [Parameter(Position=9, Mandatory=0)] + [string]$scriptPath, + [Parameter(Position=10,Mandatory=0)] + [switch]$detailedDocs = $false +) + +# setting $scriptPath here, not as default argument, to support calling as "powershell -File psake.ps1" +if (!$scriptPath) { + $scriptPath = $(Split-Path -parent $MyInvocation.MyCommand.path) +} + +# '[p]sake' is the same as 'psake' but $Error is not polluted +remove-module [p]sake +import-module (join-path $scriptPath psake.psm1) +if ($help) { + Get-Help Invoke-psake -full + return +} + +if ($buildFile -and (-not(test-path $buildFile))) { + $absoluteBuildFile = (join-path $scriptPath $buildFile) + if (test-path $absoluteBuildFile) { + $buildFile = $absoluteBuildFile + } +} + +Invoke-psake $buildFile $taskList $framework $docs $parameters $properties $initialization $nologo $detailedDocs diff --git a/tools/PSake/psake.psd1 b/tools/PSake/psake.psd1 new file mode 100644 index 0000000..3d01672 --- /dev/null +++ b/tools/PSake/psake.psd1 @@ -0,0 +1,31 @@ +@{ + ModuleToProcess = 'psake.psm1' + ModuleVersion = '4.6.0' + GUID = 'cfb53216-072f-4a46-8975-ff7e6bda05a5' + Author = 'James Kovacs' + Copyright = 'Copyright (c) 2012-16 James Kovacs, Damian Hickey and Contributors' + PowerShellVersion = '2.0' + Description = 'psake is a build automation tool written in PowerShell.' + FunctionsToExport = @('Invoke-psake', + 'Invoke-Task', + 'Get-PSakeScriptTasks', + 'Task', + 'Properties', + 'Include', + 'FormatTaskName', + 'TaskSetup', + 'TaskTearDown', + 'Framework', + 'Assert', + 'Exec') + VariablesToExport = 'psake' + + PrivateData = @{ + PSData = @{ + LicenseUri = 'https://github.com/psake/psake/blob/master/license.txt' + ProjectUri = 'https://github.com/psake/psake' + Tags = @('Build', 'Task') + IconUri = 'https://raw.githubusercontent.com/psake/graphics/master/png/psake-single-icon-teal-bg-256x256.png' + } + } +} diff --git a/tools/PSake/psake.psm1 b/tools/PSake/psake.psm1 new file mode 100644 index 0000000..e5f8f3c --- /dev/null +++ b/tools/PSake/psake.psm1 @@ -0,0 +1,925 @@ +# psake +# Copyright (c) 2012 James Kovacs +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +#Requires -Version 2.0 + +if ($PSVersionTable.PSVersion.Major -ge 3) +{ + $script:IgnoreError = 'Ignore' +} +else +{ + $script:IgnoreError = 'SilentlyContinue' +} + +#-- Public Module Functions --# + +# .ExternalHelp psake.psm1-help.xml +function Invoke-Task +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)] [string]$taskName + ) + + Assert $taskName ($msgs.error_invalid_task_name) + + $taskKey = $taskName.ToLower() + + if ($currentContext.aliases.Contains($taskKey)) { + $taskName = $currentContext.aliases.$taskKey.Name + $taskKey = $taskName.ToLower() + } + + $currentContext = $psake.context.Peek() + + Assert ($currentContext.tasks.Contains($taskKey)) ($msgs.error_task_name_does_not_exist -f $taskName) + + if ($currentContext.executedTasks.Contains($taskKey)) { return } + + Assert (!$currentContext.callStack.Contains($taskKey)) ($msgs.error_circular_reference -f $taskName) + + $currentContext.callStack.Push($taskKey) + + $task = $currentContext.tasks.$taskKey + + $precondition_is_valid = & $task.Precondition + + if (!$precondition_is_valid) { + WriteColoredOutput ($msgs.precondition_was_false -f $taskName) -foregroundcolor Cyan + } else { + if ($taskKey -ne 'default') { + + if ($task.PreAction -or $task.PostAction) { + Assert ($task.Action -ne $null) ($msgs.error_missing_action_parameter -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.config.taskNameFormat -is [ScriptBlock]) { + & $currentContext.config.taskNameFormat $taskName + } else { + WriteColoredOutput ($currentContext.config.taskNameFormat -f $taskName) -foregroundcolor Cyan + } + + foreach ($variable in $task.requiredVariables) { + Assert ((test-path "variable:$variable") -and ((get-variable $variable).Value -ne $null)) ($msgs.required_variable_not_set -f $variable, $taskName) + } + + & $task.Action + + if ($task.PostAction) { + & $task.PostAction + } + + & $currentContext.taskTearDownScriptBlock + $task.Duration = $stopwatch.Elapsed + } catch { + if ($task.ContinueOnError) { + "-"*70 + WriteColoredOutput ($msgs.continue_on_error -f $taskName,$_) -foregroundcolor Yellow + "-"*70 + $task.Duration = $stopwatch.Elapsed + } else { + throw $_ + } + } + } 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) ($msgs.postcondition_failed -f $taskName) + } + + $poppedTaskKey = $currentContext.callStack.Pop() + Assert ($poppedTaskKey -eq $taskKey) ($msgs.error_corrupt_callstack -f $taskKey,$poppedTaskKey) + + $currentContext.executedTasks.Push($taskKey) +} + +# .ExternalHelp psake.psm1-help.xml +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd), + [Parameter(Position=2,Mandatory=0)][int]$maxRetries = 0, + [Parameter(Position=3,Mandatory=0)][string]$retryTriggerErrorPattern = $null + ) + + $tryCount = 1 + + do { + try { + $global:lastexitcode = 0 + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } + break + } + catch [Exception] + { + if ($tryCount -gt $maxRetries) { + throw $_ + } + + if ($retryTriggerErrorPattern -ne $null) { + $isMatch = [regex]::IsMatch($_.Exception.Message, $retryTriggerErrorPattern) + + if ($isMatch -eq $false) { + throw $_ + } + } + + Write-Host "Try $tryCount failed, retrying again in 1 second..." + + $tryCount++ + + [System.Threading.Thread]::Sleep([System.TimeSpan]::FromSeconds(1)) + } + } + while ($true) +} + +# .ExternalHelp psake.psm1-help.xml +function Assert +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)]$conditionToCheck, + [Parameter(Position=1,Mandatory=1)]$failureMessage + ) + if (!$conditionToCheck) { + throw ("Assert: " + $failureMessage) + } +} + +# .ExternalHelp psake.psm1-help.xml +function Task +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$name = $null, + [Parameter(Position=1,Mandatory=0)][scriptblock]$action = $null, + [Parameter(Position=2,Mandatory=0)][scriptblock]$preaction = $null, + [Parameter(Position=3,Mandatory=0)][scriptblock]$postaction = $null, + [Parameter(Position=4,Mandatory=0)][scriptblock]$precondition = {$true}, + [Parameter(Position=5,Mandatory=0)][scriptblock]$postcondition = {$true}, + [Parameter(Position=6,Mandatory=0)][switch]$continueOnError = $false, + [Parameter(Position=7,Mandatory=0)][string[]]$depends = @(), + [Parameter(Position=8,Mandatory=0)][string[]]$requiredVariables = @(), + [Parameter(Position=9,Mandatory=0)][string]$description = $null, + [Parameter(Position=10,Mandatory=0)][string]$alias = $null + ) + if ($name -eq 'default') { + Assert (!$action) ($msgs.error_default_task_cannot_have_action) + } + + $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 = $psake.context.Peek() + + Assert (!$currentContext.tasks.ContainsKey($taskKey)) ($msgs.error_duplicate_task_name -f $name) + + $currentContext.tasks.$taskKey = $newTask + + if($alias) + { + $aliasKey = $alias.ToLower() + + Assert (!$currentContext.aliases.ContainsKey($aliasKey)) ($msgs.error_duplicate_alias_name -f $alias) + + $currentContext.aliases.$aliasKey = $newTask + } +} + +# .ExternalHelp psake.psm1-help.xml +function Properties { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$properties + ) + $psake.context.Peek().properties += $properties +} + +# .ExternalHelp psake.psm1-help.xml +function Include { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$fileNamePathToInclude + ) + Assert (test-path $fileNamePathToInclude -pathType Leaf) ($msgs.error_invalid_include_path -f $fileNamePathToInclude) + $psake.context.Peek().includes.Enqueue((Resolve-Path $fileNamePathToInclude)); +} + +# .ExternalHelp psake.psm1-help.xml +function FormatTaskName { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)]$format + ) + $psake.context.Peek().config.taskNameFormat = $format +} + +# .ExternalHelp psake.psm1-help.xml +function TaskSetup { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$setup + ) + $psake.context.Peek().taskSetupScriptBlock = $setup +} + +# .ExternalHelp psake.psm1-help.xml +function TaskTearDown { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$teardown + ) + $psake.context.Peek().taskTearDownScriptBlock = $teardown +} + +# .ExternalHelp psake.psm1-help.xml +function Framework { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$framework + ) + $psake.context.Peek().config.framework = $framework + ConfigureBuildEnvironment +} + +# .ExternalHelp psake.psm1-help.xml +function Get-PSakeScriptTasks { + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = 0)][string] $buildFile + ) + + if (!$buildFile) { + $buildFile = $psake.config_default.buildFileName + } + + try + { + ExecuteInBuildFileScope $buildFile $MyInvocation.MyCommand.Module { + param($currentContext, $module) + return GetTasksFromContext $currentContext + } + + } finally { + + CleanupEnvironment + } +} + +# .ExternalHelp psake.psm1-help.xml +function Invoke-psake { + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = 0)][string] $buildFile, + [Parameter(Position = 1, Mandatory = 0)][string[]] $taskList = @(), + [Parameter(Position = 2, Mandatory = 0)][string] $framework, + [Parameter(Position = 3, Mandatory = 0)][switch] $docs = $false, + [Parameter(Position = 4, Mandatory = 0)][hashtable] $parameters = @{}, + [Parameter(Position = 5, Mandatory = 0)][hashtable] $properties = @{}, + [Parameter(Position = 6, Mandatory = 0)][alias("init")][scriptblock] $initialization = {}, + [Parameter(Position = 7, Mandatory = 0)][switch] $nologo = $false, + [Parameter(Position = 8, Mandatory = 0)][switch] $detailedDocs = $false, + [Parameter(Position = 9, Mandatory = 0)][switch] $notr = $false # disable time report + ) + try { + if (-not $nologo) { + "psake version {0}`nCopyright (c) 2010-2014 James Kovacs & Contributors`n" -f $psake.version + } + + if (!$buildFile) { + $buildFile = $psake.config_default.buildFileName + } + elseif (!(test-path $buildFile -pathType Leaf) -and (test-path $psake.config_default.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 = $psake.config_default.buildFileName + } + + ExecuteInBuildFileScope $buildFile $MyInvocation.MyCommand.Module { + param($currentContext, $module) + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + + if ($docs -or $detailedDocs) { + WriteDocumentation($detailedDocs) + 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 { + throw $msgs.error_no_default_task + } + + WriteColoredOutput ("`n" + $msgs.build_success + "`n") -foregroundcolor Green + + $stopwatch.Stop() + if (-not $notr) { + WriteTaskTimeSummary $stopwatch.Elapsed + } + } + + $psake.build_success = $true + + } catch { + $currentConfig = GetCurrentConfigurationOrDefault + if ($currentConfig.verboseError) { + $error_message = "{0}: An Error Occurred. See Error Details Below: `n" -f (Get-Date) + $error_message += ("-" * 70) + "`n" + $error_message += "Error: {0}`n" -f (ResolveError $_ -Short) + $error_message += ("-" * 70) + "`n" + $error_message += ResolveError $_ + $error_message += ("-" * 70) + "`n" + $error_message += "Script Variables" + "`n" + $error_message += ("-" * 70) + "`n" + $error_message += get-variable -scope script | format-table | out-string + } else { + # ($_ | Out-String) gets error messages with source information included. + $error_message = "Error: {0}: `n{1}" -f (Get-Date), (ResolveError $_ -Short) + } + + $psake.build_success = $false + + # if we are running in a nested scope (i.e. running a psake script from a psake 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 = ($psake.context.count -gt 1) + if ( $inNestedScope ) { + throw $_ + } else { + if (!$psake.run_by_psake_build_tester) { + WriteColoredOutput $error_message -foregroundcolor Red + } + } + } finally { + CleanupEnvironment + } +} + +#-- Private Module Functions --# +function WriteColoredOutput { + param( + [string] $message, + [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 + } + } + + $message + + if ($previousColor -ne $null) { + $Host.UI.RawUI.ForegroundColor = $previousColor + } +} + +function LoadModules { + $currentConfig = $psake.context.peek().config + if ($currentConfig.modules) { + + $scope = $currentConfig.moduleScope + + $global = [string]::Equals($scope, "global", [StringComparison]::CurrentCultureIgnoreCase) + + $currentConfig.modules | foreach { + resolve-path $_ | foreach { + "Loading module: $_" + $module = import-module $_ -passthru -DisableNameChecking -global:$global + if (!$module) { + throw ($msgs.error_loading_module -f $_.Name) + } + } + } + "" + } +} + +function LoadConfiguration { + param( + [string] $configdir = $PSScriptRoot + ) + + $psakeConfigFilePath = (join-path $configdir "psake-config.ps1") + + if (test-path $psakeConfigFilePath -pathType Leaf) { + try { + $config = GetCurrentConfigurationOrDefault + . $psakeConfigFilePath + } catch { + throw "Error Loading Configuration from psake-config.ps1: " + $_ + } + } +} + +function GetCurrentConfigurationOrDefault() { + if ($psake.context.count -gt 0) { + return $psake.context.peek().config + } else { + return $psake.config_default + } +} + +function CreateConfigurationForNewContext { + param( + [string] $buildFile, + [string] $framework + ) + + $previousConfig = GetCurrentConfigurationOrDefault + + $config = new-object psobject -property @{ + buildFileName = $previousConfig.buildFileName; + framework = $previousConfig.framework; + taskNameFormat = $previousConfig.taskNameFormat; + verboseError = $previousConfig.verboseError; + coloredOutput = $previousConfig.coloredOutput; + modules = $previousConfig.modules; + moduleScope = $previousConfig.moduleScope; + } + + if ($framework) { + $config.framework = $framework; + } + + if ($buildFile) { + $config.buildFileName = $buildFile; + } + + return $config +} + +function ConfigureBuildEnvironment { + $framework = $psake.context.peek().config.framework + if ($framework -cmatch '^((?:\d+\.\d+)(?:\.\d+){0,1})(x86|x64){0,1}$') { + $versionPart = $matches[1] + $bitnessPart = $matches[2] + } else { + throw ($msgs.error_invalid_framework -f $framework) + } + $versions = $null + $buildToolsVersions = $null + switch ($versionPart) { + '1.0' { + $versions = @('v1.0.3705') + } + '1.1' { + $versions = @('v1.1.4322') + } + '2.0' { + $versions = @('v2.0.50727') + } + '3.0' { + $versions = @('v2.0.50727') + } + '3.5' { + $versions = @('v3.5', 'v2.0.50727') + } + '4.0' { + $versions = @('v4.0.30319') + } + {($_ -eq '4.5.1') -or ($_ -eq '4.5.2')} { + $versions = @('v4.0.30319') + $buildToolsVersions = @('14.0', '12.0') + } + {($_ -eq '4.6') -or ($_ -eq '4.6.1')} { + $versions = @('v4.0.30319') + $buildToolsVersions = @('14.0') + } + + default { + throw ($msgs.error_unknown_framework -f $versionPart, $framework) + } + } + + $bitness = 'Framework' + if ($versionPart -ne '1.0' -and $versionPart -ne '1.1') { + switch ($bitnessPart) { + 'x86' { + $bitness = 'Framework' + $buildToolsKey = 'MSBuildToolsPath32' + } + 'x64' { + $bitness = 'Framework64' + $buildToolsKey = 'MSBuildToolsPath' + } + { [string]::IsNullOrEmpty($_) } { + $ptrSize = [System.IntPtr]::Size + switch ($ptrSize) { + 4 { + $bitness = 'Framework' + $buildToolsKey = 'MSBuildToolsPath32' + } + 8 { + $bitness = 'Framework64' + $buildToolsKey = 'MSBuildToolsPath' + } + default { + throw ($msgs.error_unknown_pointersize -f $ptrSize) + } + } + } + default { + throw ($msgs.error_unknown_bitnesspart -f $bitnessPart, $framework) + } + } + } + $frameworkDirs = @() + if ($buildToolsVersions -ne $null) { + foreach($ver in $buildToolsVersions) { + if (Test-Path "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$ver") { + $frameworkDirs += (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$ver" -Name $buildToolsKey).$buildToolsKey + } + } + } + $frameworkDirs = $frameworkDirs + @($versions | foreach { "$env:windir\Microsoft.NET\$bitness\$_\" }) + + for ($i = 0; $i -lt $frameworkDirs.Count; $i++) { + $dir = $frameworkDirs[$i] + if ($dir -Match "\$\(Registry:HKEY_LOCAL_MACHINE(.*?)@(.*)\)") { + $key = "HKLM:" + $matches[1] + $name = $matches[2] + $dir = (Get-ItemProperty -Path $key -Name $name).$name + $frameworkDirs[$i] = $dir + } + } + + $frameworkDirs | foreach { Assert (test-path $_ -pathType Container) ($msgs.error_no_framework_install_dir_found -f $_)} + + $env:path = ($frameworkDirs -join ";") + ";$env:path" + # 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 ExecuteInBuildFileScope { + param([string]$buildFile, $module, [scriptblock]$sb) + + # Execute the build file to set up the tasks and defaults + Assert (test-path $buildFile -pathType Leaf) ($msgs.error_build_file_not_found -f $buildFile) + + $psake.build_script_file = get-item $buildFile + $psake.build_script_dir = $psake.build_script_file.DirectoryName + $psake.build_success = $false + + $psake.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" = @(); + "includes" = new-object System.Collections.Queue; + "config" = CreateConfigurationForNewContext $buildFile $framework + }) + + LoadConfiguration $psake.build_script_dir + + set-location $psake.build_script_dir + + LoadModules + + $frameworkOldValue = $framework + . $psake.build_script_file.FullName + + $currentContext = $psake.context.Peek() + + if ($framework -ne $frameworkOldValue) { + writecoloredoutput $msgs.warning_deprecated_framework_variable -foregroundcolor Yellow + $currentContext.config.framework = $framework + } + + ConfigureBuildEnvironment + + while ($currentContext.includes.Count -gt 0) { + $includeFilename = $currentContext.includes.Dequeue() + . $includeFilename + } + + & $sb $currentContext $module +} + +function CleanupEnvironment { + if ($psake.context.Count -gt 0) { + $currentContext = $psake.context.Peek() + $env:path = $currentContext.originalEnvPath + Set-Location $currentContext.originalDirectory + $global:ErrorActionPreference = $currentContext.originalErrorActionPreference + [void] $psake.context.Pop() + } +} + +function SelectObjectWithDefault +{ + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true)] + [PSObject] + $InputObject, + [string] + $Name, + $Value + ) + + process { + if ($_ -eq $null) { $Value } + elseif ($_ | Get-Member -Name $Name) { + $_.$Name + } + elseif (($_ -is [Hashtable]) -and ($_.Keys -contains $Name)) { + $_.$Name + } + else { $Value } + } +} + +# borrowed from Jeffrey Snover http://blogs.msdn.com/powershell/archive/2006/12/07/resolve-error.aspx +# modified to better handle SQL errors +function ResolveError +{ + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true)] + $ErrorRecord=$Error[0], + [Switch] + $Short + ) + + process { + if ($_ -eq $null) { $_ = $ErrorRecord } + $ex = $_.Exception + + if (-not $Short) { + $error_message = "`nErrorRecord:{0}ErrorRecord.InvocationInfo:{1}Exception:`n{2}" + $formatted_errorRecord = $_ | format-list * -force | out-string + $formatted_invocationInfo = $_.InvocationInfo | format-list * -force | out-string + $formatted_exception = '' + + $i = 0 + while ($ex -ne $null) { + $i++ + $formatted_exception += ("$i" * 70) + "`n" + + ($ex | format-list * -force | out-string) + "`n" + $ex = $ex | SelectObjectWithDefault -Name 'InnerException' -Value $null + } + + return $error_message -f $formatted_errorRecord, $formatted_invocationInfo, $formatted_exception + } + + $lastException = @() + while ($ex -ne $null) { + $lastMessage = $ex | SelectObjectWithDefault -Name 'Message' -Value '' + $lastException += ($lastMessage -replace "`n", '') + if ($ex -is [Data.SqlClient.SqlException]) { + $lastException += "(Line [$($ex.LineNumber)] " + + "Procedure [$($ex.Procedure)] Class [$($ex.Class)] " + + " Number [$($ex.Number)] State [$($ex.State)] )" + } + $ex = $ex | SelectObjectWithDefault -Name 'InnerException' -Value $null + } + $shortException = $lastException -join ' --> ' + + $header = $null + $current = $_ + $header = (($_.InvocationInfo | + SelectObjectWithDefault -Name 'PositionMessage' -Value '') -replace "`n", ' '), + ($_ | SelectObjectWithDefault -Name 'Message' -Value ''), + ($_ | SelectObjectWithDefault -Name 'Exception' -Value '') | + ? { -not [String]::IsNullOrEmpty($_) } | + Select -First 1 + + $delimiter = '' + if ((-not [String]::IsNullOrEmpty($header)) -and + (-not [String]::IsNullOrEmpty($shortException))) + { $delimiter = ' [<<==>>] ' } + + return "$($header)$($delimiter)Exception: $($shortException)" + } +} + +function GetTasksFromContext($currentContext) { + + $docs = $currentContext.tasks.Keys | foreach-object { + + $task = $currentContext.tasks.$_ + new-object PSObject -property @{ + Name = $task.Name; + Alias = $task.Alias; + Description = $task.Description; + DependsOn = $task.DependsOn; + } + } + + return $docs +} + +function WriteDocumentation($showDetailed) { + + $currentContext = $psake.context.Peek() + + if ($currentContext.tasks.default) { + $defaultTaskDependencies = $currentContext.tasks.default.DependsOn + } else { + $defaultTaskDependencies = @() + } + + $docs = GetTasksFromContext $currentContext | + Where {$_.Name -ne 'default'} | + ForEach { + $isDefault = $null + if ($defaultTaskDependencies -contains $_.Name) { + $isDefault = $true + } + return Add-Member -InputObject $_ 'Default' $isDefault -PassThru + } + + if ($showDetailed) { + $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 WriteTaskTimeSummary($invokePsakeDuration) { + if ($psake.context.count -gt 0) { + "-" * 70 + "Build Time Report" + "-" * 70 + $list = @() + $currentContext = $psake.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 = $invokePsakeDuration + } + # 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-object { $_ } + } +} + +DATA msgs { +convertfrom-stringdata @' + error_invalid_task_name = Task name should not be null or empty string. + error_task_name_does_not_exist = Task {0} does not exist. + error_circular_reference = Circular reference found for task {0}. + error_missing_action_parameter = Action parameter must be specified when using PreAction or PostAction parameters for task {0}. + error_corrupt_callstack = Call stack was corrupt. Expected {0}, but got {1}. + error_invalid_framework = Invalid .NET Framework version, {0} specified. + error_unknown_framework = Unknown .NET Framework version, {0} specified in {1}. + error_unknown_pointersize = Unknown pointer size ({0}) returned from System.IntPtr. + error_unknown_bitnesspart = Unknown .NET Framework bitness, {0}, specified in {1}. + error_no_framework_install_dir_found = No .NET Framework installation directory found at {0}. + error_bad_command = Error executing command {0}. + error_default_task_cannot_have_action = 'default' task cannot specify an action. + error_duplicate_task_name = Task {0} has already been defined. + error_duplicate_alias_name = Alias {0} has already been defined. + error_invalid_include_path = Unable to include {0}. File not found. + error_build_file_not_found = Could not find the build file {0}. + error_no_default_task = 'default' task required. + error_loading_module = Error loading module {0}. + warning_deprecated_framework_variable = Warning: Using global variable $framework to set .NET framework version used is deprecated. Instead use Framework function or configuration file psake-config.ps1. + required_variable_not_set = Variable {0} must be set to run task {1}. + postcondition_failed = Postcondition failed for task {0}. + precondition_was_false = Precondition was false, not executing task {0}. + continue_on_error = Error in task {0}. {1} + build_success = Build Succeeded! +'@ +} + +Import-LocalizedData -BindingVariable msgs -FileName messages.psd1 -ErrorAction $script:IgnoreError + +$scriptDir = Split-Path $MyInvocation.MyCommand.Path +$manifestPath = Join-Path $scriptDir psake.psd1 +$manifest = Test-ModuleManifest -Path $manifestPath -WarningAction SilentlyContinue + +$script:psake = @{} + +$psake.version = $manifest.Version.ToString() +$psake.context = new-object system.collections.stack # holds onto the current state of all variables +$psake.run_by_psake_build_tester = $false # indicates that build is being run by psake-BuildTester +$psake.config_default = new-object psobject -property @{ + buildFileName = "default.ps1"; + framework = "4.0"; + taskNameFormat = "Executing {0}"; + verboseError = $false; + coloredOutput = $true; + modules = $null; + moduleScope = ""; +} # contains default configuration, can be overriden in psake-config.ps1 in directory with psake.psm1 or in directory with current build script + +$psake.build_success = $false # indicates that the current build was successful +$psake.build_script_file = $null # contains a System.IO.FileInfo for the current build script +$psake.build_script_dir = "" # contains a string with fully-qualified path to current build script + +LoadConfiguration + +export-modulemember -function Invoke-psake, Invoke-Task, Get-PSakeScriptTasks, Task, Properties, Include, FormatTaskName, TaskSetup, TaskTearDown, Framework, Assert, Exec -variable psake