From 37ef181662e24b1558acc2d5c6d5fa341eb56fed Mon Sep 17 00:00:00 2001 From: Macks Date: Tue, 6 Feb 2018 00:45:16 +0800 Subject: [PATCH] Initial commit --- .gitattributes | 17 + .gitignore | 55 + Docs/README.md | 63 + Docs/conditional_task.md | 22 + Docs/error_handling.md | 89 + Docs/include.md | 29 + LICENSE.txt | 23 + README.md | 131 ++ Source/Builder.Tests/README.md | 21 + .../Builder.Tests/Samples/checkvariables.ps1 | 36 + .../Builder.Tests/Samples/continueonerror.ps1 | 19 + Source/Builder.Tests/Samples/default.ps1 | 25 + Source/Builder.Tests/Samples/envpath.ps1 | 7 + Source/Builder.Tests/Samples/lastwill.ps1 | 27 + Source/Builder.Tests/Samples/nestedwill.ps1 | 12 + .../Builder.Tests/Samples/nestedwill_sub.ps1 | 11 + Source/Builder.Tests/Samples/nesting.ps1 | 17 + Source/Builder.Tests/Samples/nesting_sub1.ps1 | 1 + Source/Builder.Tests/Samples/nesting_sub2.ps1 | 1 + .../Builder.Tests/Samples/paralleltasks.ps1 | 44 + Source/Builder.Tests/Samples/parameters.ps1 | 26 + .../Samples/pass_param_string.ps1 | 27 + .../Samples/postaction_and_error.ps1 | 17 + .../Samples/preandpostaction.ps1 | 13 + .../Samples/preandpostcondition.ps1 | 23 + .../Samples/printtask_scriptblock.ps1 | 25 + .../Samples/printtask_string.ps1 | 21 + Source/Builder.Tests/Samples/properties.ps1 | 23 + .../Samples/requiredvariables.ps1 | 26 + .../Samples/tasksetupandteardown.ps1 | 24 + .../Samples/taskteardown_and_error.ps1 | 21 + Source/Builder.Tests/UnitTest/AllTests.ps1 | 196 ++ Source/Builder.Tests/UnitTest/RunAllTests.cmd | 4 + .../Builder.Tests/UnitTest/en-US/Message.psd1 | 19 + .../Get-BuildScriptTasks_should_pass.ps1 | 79 + .../bad_PreAndPostActions_should_fail.ps1 | 11 + .../specs/calling_invoke-task_should_pass.ps1 | 14 + ...rcular_dependency_in_tasks_should_fail.ps1 | 3 + .../default_task_with_action_should_fail.ps1 | 5 + .../specs/detailedDocs_should_pass.ps1 | 51 + .../UnitTest/specs/docs_should_pass.ps1 | 22 + .../specs/duplicate_alias_should_fail.ps1 | 4 + .../specs/duplicate_tasks_should_fail.ps1 | 3 + ..._depends_on_another_module_should_pass.ps1 | 5 + .../failing_postcondition_should_fail.ps1 | 13 + .../specs/missing_task_should_fail.ps1 | 5 + .../UnitTest/specs/modules/ModuleA.psm1 | 5 + .../UnitTest/specs/modules/ModuleB.psm1 | 4 + .../UnitTest/specs/modules/builder-config.ps1 | 2 + .../UnitTest/specs/modules/default.ps1 | 5 + .../specs/multiline_blocks_should_pass.ps1 | 7 + .../UnitTest/specs/nested/always_fail.ps1 | 5 + .../UnitTest/specs/nested/docs.ps1 | 13 + .../UnitTest/specs/nested/nested1.ps1 | 1 + .../UnitTest/specs/nested/nested2.ps1 | 1 + .../specs/nested/whatifpreference.ps1 | 5 + .../specs/nested_builds_should_pass.ps1 | 17 + .../nested_builds_that_fail_should_fail.ps1 | 5 + .../nonzero_lastexitcode_should_pass.ps1 | 15 + ...imple_properties_and_tasks_should_pass.ps1 | 19 + ...ith_alias_and_dependencies_should_pass.ps1 | 5 + .../specs/task_with_alias_should_pass.ps1 | 5 + .../UnitTest/specs/tasksetup_should_pass.ps1 | 17 + .../using_PreAndPostActions_should_pass.ps1 | 13 + .../using_envpath_nesting_should_fail.ps1 | 10 + .../specs/using_envpath_should_pass.ps1 | 150 ++ ...c_and_nonzero_lastexitcode_should_fail.ps1 | 5 + ...using_initialization_block_should_pass.ps1 | 23 + .../specs/using_parameters_should_pass.ps1 | 9 + .../specs/using_postcondition_should_pass.ps1 | 13 + .../specs/using_precondition_should_pass.ps1 | 13 + .../specs/using_properties_should_pass.ps1 | 13 + ...sing_required_when_not_set_should_fail.ps1 | 10 + .../using_required_when_set_should_pass.ps1 | 9 + .../specs/whatif_preference_should_pass.ps1 | 18 + .../writing_builder_variables_should_pass.ps1 | 34 + Source/Builder/Builder-Config.ps1 | 29 + Source/Builder/Builder.ps1 | 84 + Source/Builder/Builder.psd1.pstmpl | Bin 0 -> 13774 bytes Source/Builder/Builder.psm1 | 1760 +++++++++++++++++ Source/Builder/build.cmd | 14 + Source/Builder/en-US/Message.psd1 | 38 + .../en-US/about_Builder.help.txt.pstmpl | 58 + Source/Builder/zh-CN/Builder-Help_Assert.md | 26 + Source/Builder/zh-CN/Builder-Help_Die.md | 17 + Source/Builder/zh-CN/Builder-Help_EnvPath.md | 8 + Source/Builder/zh-CN/Builder-Help_Exec.md | 28 + .../Builder-Help_Get-BuildScriptTasks.md | 8 + Source/Builder/zh-CN/Builder-Help_Include.md | 9 + .../zh-CN/Builder-Help_Invoke-Builder.md | 180 ++ .../Builder/zh-CN/Builder-Help_Invoke-Task.md | 8 + .../Builder/zh-CN/Builder-Help_PrintTask.md | 71 + .../Builder/zh-CN/Builder-Help_Properties.md | 8 + Source/Builder/zh-CN/Builder-Help_Say.md | 28 + Source/Builder/zh-CN/Builder-Help_Task.md | 49 + .../Builder/zh-CN/Builder-Help_TaskSetup.md | 8 + .../zh-CN/Builder-Help_TaskTearDown.md | 8 + Source/Builder/zh-CN/Builder-Help_Will.md | 11 + Source/Builder/zh-CN/Message.psd1 | 38 + THIRD-PARTY-LICENSE.txt | 34 + Tools/PSMaml.psm1 | 788 ++++++++ Tools/PSTemplate.psm1 | 390 ++++ Tools/Robocopy.psm1 | 151 ++ Tools/build.ps1 | 272 +++ Tools/projectInfo.json | 42 + Tools/publish.ps1 | 25 + Tools/template_helpers.ps1 | 217 ++ icon.png | Bin 0 -> 2743 bytes 108 files changed, 6188 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Docs/README.md create mode 100644 Docs/conditional_task.md create mode 100644 Docs/error_handling.md create mode 100644 Docs/include.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Source/Builder.Tests/README.md create mode 100644 Source/Builder.Tests/Samples/checkvariables.ps1 create mode 100644 Source/Builder.Tests/Samples/continueonerror.ps1 create mode 100644 Source/Builder.Tests/Samples/default.ps1 create mode 100644 Source/Builder.Tests/Samples/envpath.ps1 create mode 100644 Source/Builder.Tests/Samples/lastwill.ps1 create mode 100644 Source/Builder.Tests/Samples/nestedwill.ps1 create mode 100644 Source/Builder.Tests/Samples/nestedwill_sub.ps1 create mode 100644 Source/Builder.Tests/Samples/nesting.ps1 create mode 100644 Source/Builder.Tests/Samples/nesting_sub1.ps1 create mode 100644 Source/Builder.Tests/Samples/nesting_sub2.ps1 create mode 100644 Source/Builder.Tests/Samples/paralleltasks.ps1 create mode 100644 Source/Builder.Tests/Samples/parameters.ps1 create mode 100644 Source/Builder.Tests/Samples/pass_param_string.ps1 create mode 100644 Source/Builder.Tests/Samples/postaction_and_error.ps1 create mode 100644 Source/Builder.Tests/Samples/preandpostaction.ps1 create mode 100644 Source/Builder.Tests/Samples/preandpostcondition.ps1 create mode 100644 Source/Builder.Tests/Samples/printtask_scriptblock.ps1 create mode 100644 Source/Builder.Tests/Samples/printtask_string.ps1 create mode 100644 Source/Builder.Tests/Samples/properties.ps1 create mode 100644 Source/Builder.Tests/Samples/requiredvariables.ps1 create mode 100644 Source/Builder.Tests/Samples/tasksetupandteardown.ps1 create mode 100644 Source/Builder.Tests/Samples/taskteardown_and_error.ps1 create mode 100644 Source/Builder.Tests/UnitTest/AllTests.ps1 create mode 100644 Source/Builder.Tests/UnitTest/RunAllTests.cmd create mode 100644 Source/Builder.Tests/UnitTest/en-US/Message.psd1 create mode 100644 Source/Builder.Tests/UnitTest/specs/Get-BuildScriptTasks_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/bad_PreAndPostActions_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/calling_invoke-task_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/circular_dependency_in_tasks_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/default_task_with_action_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/detailedDocs_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/docs_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/duplicate_alias_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/duplicate_tasks_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/executing_module_function_that_depends_on_another_module_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/failing_postcondition_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/missing_task_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/modules/ModuleA.psm1 create mode 100644 Source/Builder.Tests/UnitTest/specs/modules/ModuleB.psm1 create mode 100644 Source/Builder.Tests/UnitTest/specs/modules/builder-config.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/modules/default.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/multiline_blocks_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested/always_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested/docs.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested/nested1.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested/nested2.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested/whatifpreference.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested_builds_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nested_builds_that_fail_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/nonzero_lastexitcode_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/simple_properties_and_tasks_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/task_with_alias_and_dependencies_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/task_with_alias_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/tasksetup_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_PreAndPostActions_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_envpath_nesting_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_envpath_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_exec_and_nonzero_lastexitcode_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_initialization_block_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_parameters_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_postcondition_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_precondition_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_properties_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_required_when_not_set_should_fail.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/using_required_when_set_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/whatif_preference_should_pass.ps1 create mode 100644 Source/Builder.Tests/UnitTest/specs/writing_builder_variables_should_pass.ps1 create mode 100644 Source/Builder/Builder-Config.ps1 create mode 100644 Source/Builder/Builder.ps1 create mode 100644 Source/Builder/Builder.psd1.pstmpl create mode 100644 Source/Builder/Builder.psm1 create mode 100644 Source/Builder/build.cmd create mode 100644 Source/Builder/en-US/Message.psd1 create mode 100644 Source/Builder/en-US/about_Builder.help.txt.pstmpl create mode 100644 Source/Builder/zh-CN/Builder-Help_Assert.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Die.md create mode 100644 Source/Builder/zh-CN/Builder-Help_EnvPath.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Exec.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Get-BuildScriptTasks.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Include.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Invoke-Builder.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Invoke-Task.md create mode 100644 Source/Builder/zh-CN/Builder-Help_PrintTask.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Properties.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Say.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Task.md create mode 100644 Source/Builder/zh-CN/Builder-Help_TaskSetup.md create mode 100644 Source/Builder/zh-CN/Builder-Help_TaskTearDown.md create mode 100644 Source/Builder/zh-CN/Builder-Help_Will.md create mode 100644 Source/Builder/zh-CN/Message.psd1 create mode 100644 THIRD-PARTY-LICENSE.txt create mode 100644 Tools/PSMaml.psm1 create mode 100644 Tools/PSTemplate.psm1 create mode 100644 Tools/Robocopy.psm1 create mode 100644 Tools/build.ps1 create mode 100644 Tools/projectInfo.json create mode 100644 Tools/publish.ps1 create mode 100644 Tools/template_helpers.ps1 create mode 100644 icon.png diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bdb0cab --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66f2aed --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# ========================= +# Build custom +# ========================= +Credentials +Working +Releases +Temp diff --git a/Docs/README.md b/Docs/README.md new file mode 100644 index 0000000..fbd0cbd --- /dev/null +++ b/Docs/README.md @@ -0,0 +1,63 @@ +Getting started +=============== +Builder is a human friendly build automation solution. If you are looking for better control over your build process beyond what your IDE churns out, but don't want to deal with the XML drama, this is just the tool! + +Nothing beats an example: + +*Your regular MSBuild csproject* + +```xml + + + + + + + + + + +task ParallelTask1 { + $waitSec = Get-Random -Minimum 3 -Maximum 8 + Start-Sleep -Seconds $waitSec + say "ParallelTask1 has finished after $waitSec seconds" +} + +task ParallelTask2 { + $waitSec = Get-Random -Minimum 3 -Maximum 8 + Start-Sleep -Seconds $waitSec + say "ParallelTask2 has finished after $waitSec seconds" +} + +task ParallelTask1andTask2 { + $jobArray = @() + + @( + "ParallelTask1", "ParallelTask2" + ) | ForEach-Object { + $jobArray += Start-Job { + Param($scriptFile, $taskName, $builderPath) + + ipmo $builderPath + Invoke-Builder $scriptFile -TaskList $taskName -NoLogo + } -ArgumentList $BuildEnv.BuildScriptFile.FullName, $_, $BuildEnv.ModulePath + } + + Wait-Job $jobArray | Receive-Job +} + +task default -depends ParallelTask1andTask2 diff --git a/Source/Builder.Tests/Samples/parameters.ps1 b/Source/Builder.Tests/Samples/parameters.ps1 new file mode 100644 index 0000000..d887731 --- /dev/null +++ b/Source/Builder.Tests/Samples/parameters.ps1 @@ -0,0 +1,26 @@ +<# + Demonstrates using the `-Parameters` parameter. + + The 'Parameters' parameter is a hashtable that can be accessed as variables in the build script. + + This script assumes that the 'Parameters' parameter is a hashtable containing: + - 'p1' = 'v1' + - 'p2' = 'v2' + + Notice that these keys in the 'Parameters' hashtable can be directly used in the script as variables. + + To try out, run this command in Powershell: + + .\Builder.ps1 ..\Builder.Tests\Samples\parameters.ps1 -Parameters @{'p1' = 'v1'; 'p2' = 'v2'} +#> + +properties { + $my_property = $p1 + $p2 +} + +task default -depends TestParams + +task TestParams { + say ('Value of $my_property is {0}' -f $my_property) + assert ($my_property -eq 'v1v2') "Run with -parameters @{'p1' = 'v1'; 'p2' = 'v2'}" +} diff --git a/Source/Builder.Tests/Samples/pass_param_string.ps1 b/Source/Builder.Tests/Samples/pass_param_string.ps1 new file mode 100644 index 0000000..7960c14 --- /dev/null +++ b/Source/Builder.Tests/Samples/pass_param_string.ps1 @@ -0,0 +1,27 @@ +<# + Demonstrates using the `-Parameters` parameter. + + The 'Parameters' parameter is a hashtable that can be accessed as variables in the build script. + + This script assumes that the 'Parameters' parameter is a hashtable containing: + - 'buildConfiguration' = 'release' + + Notice that these keys in the 'Parameters' hashtable can be directly used in the script as variables. + + To try out, run this command in Powershell: + + .\Builder.ps1 ..\Builder.Tests\Samples\pass_param_string.ps1 -Parameters @{'buildConfiguration' = 'release'} +#> + +properties { + $buildOutputPath = ".\bin\$buildConfiguration" +} + +task default -depends DoRelease + +task DoRelease { + assert ("$buildConfiguration" -ne $null) '$buildConfiguration should not be null' + assert ("$buildConfiguration" -eq 'Release') 'Call with -parameters @{ buildConfiguration = "Release" }' + + say ("This will build output into path '{0}' for build configuration '{1}'" -f $buildOutputPath, $buildConfiguration) +} diff --git a/Source/Builder.Tests/Samples/postaction_and_error.ps1 b/Source/Builder.Tests/Samples/postaction_and_error.ps1 new file mode 100644 index 0000000..a62b698 --- /dev/null +++ b/Source/Builder.Tests/Samples/postaction_and_error.ps1 @@ -0,0 +1,17 @@ +<# + PostAction only runs if Action doesn't fail. +#> + +task default -depends Test + +task Test -depends Compile, Clean -PreAction { "Pre test" } -Action { + assert $false "This fails." +} -PostAction { "I never gets executed" } + +task Compile -depends Clean { + "Compile" +} + +task Clean { + "Clean" +} diff --git a/Source/Builder.Tests/Samples/preandpostaction.ps1 b/Source/Builder.Tests/Samples/preandpostaction.ps1 new file mode 100644 index 0000000..a969f57 --- /dev/null +++ b/Source/Builder.Tests/Samples/preandpostaction.ps1 @@ -0,0 +1,13 @@ +task default -depends Test + +task Test -depends Compile, Clean -preaction { "Pre-Test" } -action { + "Test" +} -postaction { "Post-Test" } + +task Compile -depends Clean { + "Compile" +} + +task Clean { + "Clean" +} \ No newline at end of file diff --git a/Source/Builder.Tests/Samples/preandpostcondition.ps1 b/Source/Builder.Tests/Samples/preandpostcondition.ps1 new file mode 100644 index 0000000..4f8df55 --- /dev/null +++ b/Source/Builder.Tests/Samples/preandpostcondition.ps1 @@ -0,0 +1,23 @@ +properties { + $runTaskA = $false + $taskBSucceded = $true +} + +task default -depends TaskC + +task TaskA -precondition { $runTaskA -eq $true } { + "TaskA executed" +} + +task TaskB -depends TaskA { + "TaskB executed" +} +<# +task TaskB -postcondition { $taskBSucceded -eq $true } { + "TaskB executed" +} +#> + +task TaskC -depends TaskA,TaskB { + "TaskC executed." +} \ No newline at end of file diff --git a/Source/Builder.Tests/Samples/printtask_scriptblock.ps1 b/Source/Builder.Tests/Samples/printtask_scriptblock.ps1 new file mode 100644 index 0000000..e3a1251 --- /dev/null +++ b/Source/Builder.Tests/Samples/printtask_scriptblock.ps1 @@ -0,0 +1,25 @@ +properties { + $testMessage = 'Executed Test!' + $compileMessage = 'Executed Compile!' + $cleanMessage = 'Executed Clean!' +} + +task default -depends Test + +printTask { + param($taskName) + + Write-Host $taskName -ForegroundColor Green +} + +task Test -depends Compile, Clean { + $testMessage +} + +task Compile -depends Clean { + $compileMessage +} + +task Clean { + $cleanMessage +} \ No newline at end of file diff --git a/Source/Builder.Tests/Samples/printtask_string.ps1 b/Source/Builder.Tests/Samples/printtask_string.ps1 new file mode 100644 index 0000000..289f4d0 --- /dev/null +++ b/Source/Builder.Tests/Samples/printtask_string.ps1 @@ -0,0 +1,21 @@ +properties { + $testMessage = 'Executed Test!' + $compileMessage = 'Executed Compile!' + $cleanMessage = 'Executed Clean!' +} + +task default -depends Test + +printTask "-------{0}-------" + +task Test -depends Compile, Clean { + $testMessage +} + +task Compile -depends Clean { + $compileMessage +} + +task Clean { + $cleanMessage +} \ No newline at end of file diff --git a/Source/Builder.Tests/Samples/properties.ps1 b/Source/Builder.Tests/Samples/properties.ps1 new file mode 100644 index 0000000..99e2dbf --- /dev/null +++ b/Source/Builder.Tests/Samples/properties.ps1 @@ -0,0 +1,23 @@ +<# + You can override properties defined in the build script from the command line. + + In this example, you will override the value of 'x' and 'y' in the build script. + + To try out, run the following command in Powershell: + + .\Builder.ps1 ..\Builder.Tests\Samples\properties.ps1 -Properties @{ 'x' = 1; 'y' = 2 } +#> + +properties { + $x = $null + $y = $null + $z = $null +} + +task default -depends TestProperties + +task TestProperties { + assert ($x -eq 1) "x should be 1. Run with -properties @{x = 1; y = 2}" + assert ($y -eq 2) "y should be 2. Run with -properties @{x = 1; y = 2}" + assert ($z -eq $null) "z should be null" +} diff --git a/Source/Builder.Tests/Samples/requiredvariables.ps1 b/Source/Builder.Tests/Samples/requiredvariables.ps1 new file mode 100644 index 0000000..069c5b9 --- /dev/null +++ b/Source/Builder.Tests/Samples/requiredvariables.ps1 @@ -0,0 +1,26 @@ +<# + Instead of using `assert`, the parameter 'RequiredVariables' offers an easier way to check that + the variables you need are not null before executing the task. + + If the variable you require is null or undefined, an error is raised. + + To try out, run this in Powershell: + + .\Builder.ps1 ..\Builder.Tests\Samples\requiredvariables.ps1 -properties @{ x=1; y=2; z=3 } +#> + +properties { + $x = $null + $y = $null + $z = $null +} + +task default -depends TestRequiredVariables + +# Tip: you can separate multiple lines using ` +task TestRequiredVariables ` + -description "This task shows how to make a variable required to run task. Run this script with -properties @{x = 1; y = 2; z = 3}" ` + -requiredVariables x, y, z ` +{ + say ("You will only see me if the variables x, y and z are defined!") +} diff --git a/Source/Builder.Tests/Samples/tasksetupandteardown.ps1 b/Source/Builder.Tests/Samples/tasksetupandteardown.ps1 new file mode 100644 index 0000000..88ad2a4 --- /dev/null +++ b/Source/Builder.Tests/Samples/tasksetupandteardown.ps1 @@ -0,0 +1,24 @@ +<# + The scripts for `taskSetup` and `taskTearDown` gets executed before and after each task respectively. + + To try out: + .\build.cmd ..\Builder.Tests\Samples\tasksetupandteardown.ps1 +#> + +taskSetup { + "Executing task setup" +} + +taskTearDown { + "Executing task tear down" +} + +task default -depends TaskB + +task TaskA { + "TaskA executed" +} + +task TaskB -depends TaskA { + "TaskB executed" +} diff --git a/Source/Builder.Tests/Samples/taskteardown_and_error.ps1 b/Source/Builder.Tests/Samples/taskteardown_and_error.ps1 new file mode 100644 index 0000000..5b2f944 --- /dev/null +++ b/Source/Builder.Tests/Samples/taskteardown_and_error.ps1 @@ -0,0 +1,21 @@ +<# + If an error occured in a task, `taskTearDown` will not work for that task and beyond. +#> + +task default -depends Test + +task Test -depends Compile, Clean { + assert $false "This fails." +} + +task Compile -depends Clean { + "Compile" +} + +task Clean { + "Clean" +} + +taskTearDown { + "$($BuildEnv.Context.Peek().CurrentTaskName) Tear Down" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/AllTests.ps1 b/Source/Builder.Tests/UnitTest/AllTests.ps1 new file mode 100644 index 0000000..5694d19 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/AllTests.ps1 @@ -0,0 +1,196 @@ +#Requires -Version 2.0 + +$script:IgnoreError = $( + if ($PSVersionTable.PSVersion.Major -ge 3) { 'Ignore' } + else { 'SilentlyContinue' } +) + +####################################################################### +# Data +####################################################################### + +DATA PBUnitTestLocalizedData +{ + ConvertFrom-StringData @' +# ---- [ Localized Data ] --------------------------------------------- + +UnitTest = Unit Test +Err_BadSpecFileName = Invalid unit test pecification file name. A specification file should end with '_should_pass' or '_should_fail: {0} +Header = Running unit test on Builder +Passed = PASSED +Failed = FAILED +HasFailedTest = One or more of the build files failed x_x +AllTestsSuccessful = All build specs passed (^_^ +DetailHeader = Test Results +ConclusionHeader = Conclusion + +# ---- [ /Localized Data ] -------------------------------------------- +'@ +} +Import-LocalizedData -BindingVariable PBUnitTestLocalizedData -FileName Message.psd1 -ErrorAction $script:IgnoreError + + +####################################################################### +# Private Module Functions +####################################################################### + +function RunBuilds +{ + $buildFileList = dir (Join-Path $PSScriptRoot -ChildPath 'specs/*.ps1') + $testResults = @() + + # Add a fake build file to the $buildFiles array so that we can verify + # that Invoke-Builder fails + $nonExistantBuildFile = '' | select Name, FullName + $nonExistantBuildFile.Name = "specifying_a_non_existant_buildfile_should_fail.ps1" + $nonExistantBuildFile.FullName = "c:\specifying_a_non_existant_buildfile_should_fail.ps1" + $buildFileList += $nonExistantBuildFile + + PrintOutput '[' -ForegroundColor Gray -NoNewLine + foreach ($buildFile in $buildFileList) + { + $testResult = "" | select Name, Result + $testResult.Name = $buildFile.Name + Invoke-Builder $buildFile.FullName -Parameters @{ + 'p1'='v1' + 'p2'='v2' + } -Properties @{ + 'x'='1' + 'y'='2' + } -Initialization { + if (-not $container) + { + $container = @{}; + } + $container.bar = "bar" + $container.baz = "baz" + $bar = 2 + $baz = 3 + } | Out-Null + $testResult.Result = GetResult $buildFile.Name -BuildSuccess:$BuildEnv.BuildSuccess + $testResults += $testResult + + if ($testResult.Result -eq 'Passed') + { + PrintOutput '-' -ForegroundColor Green -NoNewLine + } + else + { + PrintOutput 'x' -ForegroundColor Red -NoNewLine + } + } + PrintOutput ']' -ForegroundColor Gray + + $testResults +} + +function GetResult +{ + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [string]$FileName, + + [Parameter(Mandatory = $false)] + [switch]$BuildSuccess + ) + + $shouldSucceed = $null + + if ($FileName.EndsWith("_should_pass.ps1")) + { + $shouldSucceed = $true + } + elseif ($FileName.EndsWith("_should_fail.ps1")) + { + $shouldSucceed = $false + } + else + { + $errRecord = New-ErrorRecord -Message ($PBUnitTestLocalizedData.Err_BadSpecFileName -f $FileName) -Exception FormatException -ErrorID BadSpecFileName -ErrorCategory InvalidData + $PSCmdlet.ThrowTerminatingError($errRecord) + } + + if ($BuildSuccess -eq $shouldSucceed) { 'Passed' } + else { 'Failed' } +} + +function PrintOutput +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 1)] + [string]$Text, + + [Parameter(Mandatory = $false)] + [ConsoleColor]$ForegroundColor = 'White', + + [Parameter(Mandatory = $false)] + [switch]$NoNewLine + ) + + if (($Host.UI -ne $null) -and + ($Host.UI.RawUI -ne $null) -and + ($Host.UI.RawUI.ForegroundColor -ne $null)) + { + Write-Host $Text -ForegroundColor $ForegroundColor -NoNewLine:$NoNewLine + } + else + { + Write-Output $Text + } +} + + +####################################################################### +# Main +####################################################################### + +PrintOutput ('=' * $PBUnitTestLocalizedData.UnitTest.Length) +PrintOutput $PBUnitTestLocalizedData.UnitTest +PrintOutput ('=' * $PBUnitTestLocalizedData.UnitTest.Length) + +# build with progress bar +Write-Output '' +PrintOutput $PBUnitTestLocalizedData.Header -ForegroundColor Cyan + +Remove-Module Builder -ErrorAction $script:IgnoreError +$builderPsmPath = Join-Path (Split-Path $PSScriptRoot -Parent) -ChildPath '../Builder/Builder.psm1' +Import-Module $builderPsmPath +$BuildEnv.RunByUnitTest = $true +$results = RunBuilds +Remove-Module Builder + +# show results +Write-Output '' +PrintOutput $PBUnitTestLocalizedData.DetailHeader -ForegroundColor Cyan +PrintOutput ('-' * $PBUnitTestLocalizedData.DetailHeader.Length) -ForegroundColor Cyan + +$results | sort 'Name' | ForEach-Object { + $testName = $_.Name.Substring(0, $_.Name.Length - 4) # .ps1 + if ($_.Result -eq 'Passed') + { + PrintOutput ('[{0}] {1}' -f $PBUnitTestLocalizedData.Passed, $testName) -ForegroundColor Green + } + else + { + PrintOutput ('[{0}] {1}' -f $PBUnitTestLocalizedData.Failed, $testName) -ForegroundColor Red + } +} + +# conclude +Write-Output '' +PrintOutput $PBUnitTestLocalizedData.ConclusionHeader -ForegroundColor Cyan +PrintOutput ('-' * $PBUnitTestLocalizedData.ConclusionHeader.Length) -ForegroundColor Cyan + +$failures = $results | where { $_.Result -eq 'Failed' } +if ($failures) +{ + PrintOutput $PBUnitTestLocalizedData.HasFailedTest -ForegroundColor Red + exit 1 +} +else +{ + PrintOutput $PBUnitTestLocalizedData.AllTestsSuccessful -ForegroundColor Green + exit 0 +} diff --git a/Source/Builder.Tests/UnitTest/RunAllTests.cmd b/Source/Builder.Tests/UnitTest/RunAllTests.cmd new file mode 100644 index 0000000..0fd11ff --- /dev/null +++ b/Source/Builder.Tests/UnitTest/RunAllTests.cmd @@ -0,0 +1,4 @@ +@echo off +rem Run unit test +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0\AllTests.ps1'" +pause \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/en-US/Message.psd1 b/Source/Builder.Tests/UnitTest/en-US/Message.psd1 new file mode 100644 index 0000000..f7b352d --- /dev/null +++ b/Source/Builder.Tests/UnitTest/en-US/Message.psd1 @@ -0,0 +1,19 @@ +# Localized 25/12/2016 7:08 PM (GMT) 303:4.80.0411 Message.psd1 +# Builder/UnitTest PBUnitTestLocalizedData.en-US + +ConvertFrom-StringData @' + +# ----[ Localized Data ]------------------------------------------------------------- + +UnitTest = Unit Test +Err_BadSpecFileName = Invalid unit test pecification file name. A specification file should end with '_should_pass' or '_should_fail: {0} +Header = Running unit test on Builder +Passed = PASSED +Failed = FAILED +HasFailedTest = One or more of the build files failed (x_x +AllTestsSuccessful = All build specs passed ^o^ +DetailHeader = Test Results +ConclusionHeader = Conclusion + +# ----[ /Localized Data ]------------------------------------------------------------ +'@ diff --git a/Source/Builder.Tests/UnitTest/specs/Get-BuildScriptTasks_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/Get-BuildScriptTasks_should_pass.ps1 new file mode 100644 index 0000000..f6d85a7 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/Get-BuildScriptTasks_should_pass.ps1 @@ -0,0 +1,79 @@ +task default -depends CheckGetBuildScriptTasks + +function Assert-EqualArrays($a, $b, $message) +{ + $differences = @(Compare-Object $a $b -SyncWindow 0) + + if ($differences.Length -gt 0) + { + $differences + } + + assert ($differences.Length -eq 0) "$message : $($differences.Length) differences found." +} + +function Assert-TaskEqual($t1, $t2) +{ + assert ($t1.Name -eq $t2.Name) "Task names do not match: $($t1.Name) vs $($t2.Name)" + assert ($t1.Alias -eq $t2.Alias) "Task aliases do not match for task $($t1.Name): $($t1.Alias) vs $($t2.Alias)" + assert ($t1.Description -eq $t2.Description) "Task descriptions do not match for task $($t1.Name): $($t1.Description) vs $($t2.Description)" + + Assert-EqualArrays $t1.DependsOn $t2.DependsOn "Task dependencies do not match for task $($t1.Name)" +} + +task CheckGetBuildScriptTasks { + $tasks = Get-BuildScriptTasks .\nested\docs.ps1 + $tasks = $tasks | sort -Property Name + + assert ($tasks.Length -eq 7) 'Unexpected number of tasks.' + + $taskSpecs = @( + [pscustomobject]@{ + Name = 'Compile' + Alias = '' + Description = '' + DependsOn = @('CompileSolutionA', 'CompileSolutionB') + } + [pscustomobject]@{ + Name = 'CompileSolutionA' + Alias = '' + Description = 'Compiles solution A' + DependsOn = @() + } + [pscustomobject]@{ + Name = 'CompileSolutionB' + Alias = '' + Description = '' + DependsOn = @() + } + [pscustomobject]@{ + Name = 'default' + Alias = '' + Description = '' + DependsOn = @('Compile', 'Test') + } + [pscustomobject]@{ + Name = 'IntegrationTests' + Alias = '' + Description = '' + DependsOn = @() + } + [pscustomobject]@{ + Name = 'Test' + Alias = '' + Description = '' + DependsOn = @('UnitTests', 'IntegrationTests') + } + [pscustomobject]@{ + Name = 'UnitTests' + Alias = 'ut' + Description = '' + DependsOn = @() + } + ) + + for ($i = 0; $i -lt $taskSpecs.Count; $i++) + { + Assert-TaskEqual $tasks[$i] $taskSpecs[$i] + } +} diff --git a/Source/Builder.Tests/UnitTest/specs/bad_PreAndPostActions_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/bad_PreAndPostActions_should_fail.ps1 new file mode 100644 index 0000000..facdfb8 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/bad_PreAndPostActions_should_fail.ps1 @@ -0,0 +1,11 @@ +task default -depends Test + +task Test -depends Compile, Clean -PreAction {"Pre-Test"} -PostAction {"Post-Test"} + +task Compile -depends Clean { + "Compile" +} + +task Clean { + "Clean" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/calling_invoke-task_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/calling_invoke-task_should_pass.ps1 new file mode 100644 index 0000000..01f1b4a --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/calling_invoke-task_should_pass.ps1 @@ -0,0 +1,14 @@ +task default -depends A, B + +task A { +} + +task B { + "inside task B before calling task C" + Invoke-Task C + "inside task B after calling task C" +} + +task C { + "i am task c" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/circular_dependency_in_tasks_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/circular_dependency_in_tasks_should_fail.ps1 new file mode 100644 index 0000000..b23cefa --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/circular_dependency_in_tasks_should_fail.ps1 @@ -0,0 +1,3 @@ +task default -depends A +task A -depends B { } +task B -depends A { } diff --git a/Source/Builder.Tests/UnitTest/specs/default_task_with_action_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/default_task_with_action_should_fail.ps1 new file mode 100644 index 0000000..5d20507 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/default_task_with_action_should_fail.ps1 @@ -0,0 +1,5 @@ +task default { + "Starting to do stuff..." + "Adding stuff... 1 + 1 =" + (1+1) + "Stuff done!" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/detailedDocs_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/detailedDocs_should_pass.ps1 new file mode 100644 index 0000000..ab78dd4 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/detailedDocs_should_pass.ps1 @@ -0,0 +1,51 @@ +task default -depends CheckDetailedDocs + +task CheckDetailedDocs { + $doc = Invoke-Builder .\nested\docs.ps1 -DetailDocs -NoLogo | Out-String + + $expectedDoc = @" + + +Name : Compile +Alias : +Description : +Depends On : CompileSolutionA, CompileSolutionB +Default : True + +Name : CompileSolutionA +Alias : +Description : Compiles solution A +Depends On : +Default : + +Name : CompileSolutionB +Alias : +Description : +Depends On : +Default : + +Name : IntegrationTests +Alias : +Description : +Depends On : +Default : + +Name : Test +Alias : +Description : +Depends On : UnitTests, IntegrationTests +Default : True + +Name : UnitTests +Alias : ut +Description : +Depends On : +Default : + + + + +"@ + + assert ($doc -eq $expectedDoc) "Unexpected doc content: $doc" +} diff --git a/Source/Builder.Tests/UnitTest/specs/docs_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/docs_should_pass.ps1 new file mode 100644 index 0000000..b5fa8ed --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/docs_should_pass.ps1 @@ -0,0 +1,22 @@ +task default -depends CheckDocs + +task CheckDocs { + $doc = Invoke-Builder .\nested\docs.ps1 -Docs -NoLogo | Out-String + + $expectedDoc = @" + +Name Alias Depends On Default Description +---- ----- ---------- ------- ----------- +Compile CompileSolutionA, CompileSolutionB True +CompileSolutionA Compiles solution A +CompileSolutionB +IntegrationTests +Test UnitTests, IntegrationTests True +UnitTests ut + + + +"@ + + assert ($doc -eq $expectedDoc) "Unexpected doc content: $doc" +} diff --git a/Source/Builder.Tests/UnitTest/specs/duplicate_alias_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/duplicate_alias_should_fail.ps1 new file mode 100644 index 0000000..00127f3 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/duplicate_alias_should_fail.ps1 @@ -0,0 +1,4 @@ +task default +task A -alias a {} +task B -alias b {} +task C -alias a {} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/duplicate_tasks_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/duplicate_tasks_should_fail.ps1 new file mode 100644 index 0000000..246201f --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/duplicate_tasks_should_fail.ps1 @@ -0,0 +1,3 @@ +task A {} +task B {} +task A {} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/executing_module_function_that_depends_on_another_module_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/executing_module_function_that_depends_on_another_module_should_pass.ps1 new file mode 100644 index 0000000..564fdc8 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/executing_module_function_that_depends_on_another_module_should_pass.ps1 @@ -0,0 +1,5 @@ +task default -depends test + +task test { + Invoke-Builder modules\default.ps1 +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/failing_postcondition_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/failing_postcondition_should_fail.ps1 new file mode 100644 index 0000000..70f12f1 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/failing_postcondition_should_fail.ps1 @@ -0,0 +1,13 @@ +task default -depends A,B,C + +task A { + "TaskA" +} + +task B -postcondition { return $false } { + "TaskB" +} + +task C { + "TaskC" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/missing_task_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/missing_task_should_fail.ps1 new file mode 100644 index 0000000..ec1d343 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/missing_task_should_fail.ps1 @@ -0,0 +1,5 @@ +task default -depends Test + +task Test -depends Compile, Clean { + say "Running test" +} diff --git a/Source/Builder.Tests/UnitTest/specs/modules/ModuleA.psm1 b/Source/Builder.Tests/UnitTest/specs/modules/ModuleA.psm1 new file mode 100644 index 0000000..d731192 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/modules/ModuleA.psm1 @@ -0,0 +1,5 @@ +function Execute-ModuleAFunction { + Execute-ModuleBFunction +} + +Export-ModuleMember -Function "Execute-ModuleAFunction" \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/modules/ModuleB.psm1 b/Source/Builder.Tests/UnitTest/specs/modules/ModuleB.psm1 new file mode 100644 index 0000000..a166615 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/modules/ModuleB.psm1 @@ -0,0 +1,4 @@ +function Execute-ModuleBFunction { +} + +Export-ModuleMember -Function "Execute-ModuleBFunction" \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/modules/builder-config.ps1 b/Source/Builder.Tests/UnitTest/specs/modules/builder-config.ps1 new file mode 100644 index 0000000..fd9b8df --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/modules/builder-config.ps1 @@ -0,0 +1,2 @@ +$config.modules = @("ModuleA.psm1", "ModuleB.psm1") +$config.moduleScope = "global" \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/modules/default.ps1 b/Source/Builder.Tests/UnitTest/specs/modules/default.ps1 new file mode 100644 index 0000000..90c2ef7 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/modules/default.ps1 @@ -0,0 +1,5 @@ +task default -depends test + +task test { + Execute-ModuleAFunction +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/multiline_blocks_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/multiline_blocks_should_pass.ps1 new file mode 100644 index 0000000..1314f7c --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/multiline_blocks_should_pass.ps1 @@ -0,0 +1,7 @@ +task default -depends doStuff + +task doStuff { + "Starting to do stuff..." + "Adding stuff... 1 + 1 =" + (1+1) + "Stuff done!" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/nested/always_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/nested/always_fail.ps1 new file mode 100644 index 0000000..64a5444 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested/always_fail.ps1 @@ -0,0 +1,5 @@ +task default -depends AlwaysFail + +task AlwaysFail { + assert $false "This should always fail." +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/nested/docs.ps1 b/Source/Builder.Tests/UnitTest/specs/nested/docs.ps1 new file mode 100644 index 0000000..48fc560 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested/docs.ps1 @@ -0,0 +1,13 @@ +task default -depends Compile, Test + +task Compile -depends CompileSolutionA, CompileSolutionB + +task Test -depends UnitTests, IntegrationTests + +task CompileSolutionA -description 'Compiles solution A' {} + +task CompileSolutionB {} + +task UnitTests -alias 'ut' {} + +task IntegrationTests {} diff --git a/Source/Builder.Tests/UnitTest/specs/nested/nested1.ps1 b/Source/Builder.Tests/UnitTest/specs/nested/nested1.ps1 new file mode 100644 index 0000000..5e3464e --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested/nested1.ps1 @@ -0,0 +1 @@ +properties { $x = 100 } task default -depends Nested1CheckX task Nested1CheckX{ assert ($x -eq 100) ('Expect $x to be 100 (actual value is {0})' -f $x) } \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/nested/nested2.ps1 b/Source/Builder.Tests/UnitTest/specs/nested/nested2.ps1 new file mode 100644 index 0000000..99ccd56 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested/nested2.ps1 @@ -0,0 +1 @@ +properties { $x = 200 } task default -depends Nested2CheckX task nested2CheckX { assert ($x -eq 200) ('Expect $x to be 100 (actual value is {0})' -f $x) } \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/nested/whatifpreference.ps1 b/Source/Builder.Tests/UnitTest/specs/nested/whatifpreference.ps1 new file mode 100644 index 0000000..9f23da7 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested/whatifpreference.ps1 @@ -0,0 +1,5 @@ +task default -depends WhatIfCheck + +task WhatIfCheck { + assert ($p1 -eq 'whatifcheck') ('Expect $p1 to be "whatifcheck" (actual value is "{0}")' -f $p1) +} diff --git a/Source/Builder.Tests/UnitTest/specs/nested_builds_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/nested_builds_should_pass.ps1 new file mode 100644 index 0000000..3c60de7 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested_builds_should_pass.ps1 @@ -0,0 +1,17 @@ +properties { + $x = 1 +} + +task default -depends RunNested1, RunNested2, CheckX + +task RunNested1 { + Invoke-Builder .\nested\nested1.ps1 +} + +task RunNested2 { + Invoke-Builder .\nested\nested2.ps1 +} + +task CheckX { + assert ($x -eq 1) '$x was not 1' +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/nested_builds_that_fail_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/nested_builds_that_fail_should_fail.ps1 new file mode 100644 index 0000000..0b9a229 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nested_builds_that_fail_should_fail.ps1 @@ -0,0 +1,5 @@ +task default -depends RunAlwaysFail + +task RunAlwaysFail { + Invoke-Builder .\nested\always_fail.ps1 +} diff --git a/Source/Builder.Tests/UnitTest/specs/nonzero_lastexitcode_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/nonzero_lastexitcode_should_pass.ps1 new file mode 100644 index 0000000..e75103b --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/nonzero_lastexitcode_should_pass.ps1 @@ -0,0 +1,15 @@ +task default -depends CallExecutable + +task CallExecutable { + # this should return last exit code 1 + # $global:LASTEXITCODE + + # These examples below will result in a failed build: + # + # exec { cmd.exe /c "exit 1" } + # cmd.exe /c "exit 1" 2>&1 + # + # But this will not: + # + cmd.exe /c "exit 1" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/simple_properties_and_tasks_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/simple_properties_and_tasks_should_pass.ps1 new file mode 100644 index 0000000..cd5d71f --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/simple_properties_and_tasks_should_pass.ps1 @@ -0,0 +1,19 @@ +properties { + $testMessage = 'Executed Test!' + $compileMessage = 'Executed Compile!' + $cleanMessage = 'Executed Clean!' +} + +task default -depends Test + +task Test -depends Compile, Clean { + $testMessage +} + +task Compile -depends Clean { + $compileMessage +} + +task Clean { + $cleanMessage +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/task_with_alias_and_dependencies_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/task_with_alias_and_dependencies_should_pass.ps1 new file mode 100644 index 0000000..4aca802 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/task_with_alias_and_dependencies_should_pass.ps1 @@ -0,0 +1,5 @@ +task default -depends Task_With_Alias + +task Task_With_Alias -depends Task_Dependency -alias twa {} + +task Task_Dependency {} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/task_with_alias_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/task_with_alias_should_pass.ps1 new file mode 100644 index 0000000..afec1c2 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/task_with_alias_should_pass.ps1 @@ -0,0 +1,5 @@ +task default -depends twbdn + +task Task_With_Big_Descriptve_Name -alias twbdn { + "Doing stuff inside task with alias" +} diff --git a/Source/Builder.Tests/UnitTest/specs/tasksetup_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/tasksetup_should_pass.ps1 new file mode 100644 index 0000000..5edc007 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/tasksetup_should_pass.ps1 @@ -0,0 +1,17 @@ +taskSetup { + "executing task setup" +} + +task default -depends Compile, Test, Deploy + +task Compile { + "Compiling" +} + +task Test -depends Compile { + "Testing" +} + +task Deploy -depends Test { + "Deploying" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_PreAndPostActions_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_PreAndPostActions_should_pass.ps1 new file mode 100644 index 0000000..745fcd5 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_PreAndPostActions_should_pass.ps1 @@ -0,0 +1,13 @@ +task default -depends Test + +task Test -depends Compile, Clean -PreAction { "Pre-Test" } -Action { + "Test" +} -PostAction { "Post-Test" } + +task Compile -depends Clean { + "Compile" +} + +task Clean { + "Clean" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_envpath_nesting_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/using_envpath_nesting_should_fail.ps1 new file mode 100644 index 0000000..a7765d7 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_envpath_nesting_should_fail.ps1 @@ -0,0 +1,10 @@ +task default -depends CallMSBuild + +task CallUsingEnvPath { + Invoke-Builder -BuildFile (Join-Path $BuildEnv.BuildScriptDir -ChildPath 'using_envpath_should_pass.ps1') +} + +task CallMSBuild -depends CallUsingEnvPath { + $msBuildVersion = msbuild /version + say $msBuildVersion +} diff --git a/Source/Builder.Tests/UnitTest/specs/using_envpath_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_envpath_should_pass.ps1 new file mode 100644 index 0000000..29f346c --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_envpath_should_pass.ps1 @@ -0,0 +1,150 @@ +function Get-DotNetPath +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory, Position = 1)] + [string]$Framework, + + [Parameter()] + [switch]$Force + ) + + # bitnessPart may be null + if ($Framework -cmatch '^((?:\d+\.\d+)(?:\.\d+){0,1})(x86|x64){0,1}$') + { + $versionPart = $matches[1] + $bitnessPart = $matches[2] + } + else + { + die ('Invalid framework: {0}' -f $Framework) 'FrameworkFormatError' + } + + $versions = $null + $buildToolsVersions = $null + + # buildToolsVersions may be 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 + { + die ("Unknown or unsupported framework version '{0}': {1}" -f $versionPart, $Framework) 'UnsupportedFramework' + } + } + + $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 + { + Die ('Unknown system pointer size: {0}' -f $ptrSize) 'UnsupportedFrameworkPlatform' + } + } + } + + default + { + Die ("Unknown operating system bit size '{0}': {1}" -f $bitnessPart, $Framework) 'UnsupportedFrameworkPlatform' + } + } + } + + $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-Object { + Join-Path $env:windir -ChildPath "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 + } + } + + if (-not $Force) + { + $frameworkDirs | ForEach-Object { + Assert (Test-Path $_ -PathType Container) -ErrorMessage ('Framework was not installed: {0}' -f $_) + } + } + + $frameworkDirs +} + +envpath (Get-DotNetPath '4.0' -Force) + +task default -depends EnvPathFunction + +task EnvPathFunction { + $msBuildVersion = msbuild /version + say $msBuildVersion[0].ToLowerInvariant() + assert ($msBuildVersion[0].ToLowerInvariant().StartsWith("microsoft (r) build engine version")) "Failed to run msbuild: $msBuildVersion" +} diff --git a/Source/Builder.Tests/UnitTest/specs/using_exec_and_nonzero_lastexitcode_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/using_exec_and_nonzero_lastexitcode_should_fail.ps1 new file mode 100644 index 0000000..32dee0f --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_exec_and_nonzero_lastexitcode_should_fail.ps1 @@ -0,0 +1,5 @@ +task default -depends CallExternalWithError + +task CallExternalWithError { + exec { cmd.exe /c "exit 1" } +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_initialization_block_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_initialization_block_should_pass.ps1 new file mode 100644 index 0000000..e29de13 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_initialization_block_should_pass.ps1 @@ -0,0 +1,23 @@ +properties { + $container = @{} + $container.foo = "foo" + $container.bar = $null + $foo = 1 + $bar = 1 +} + +task default -depends TestInit + +task TestInit { + # values are: + # 1: original + # 2: overide + # 3: new + + assert ($container.foo -eq "foo") "$container.foo should be foo" + assert ($container.bar -eq "bar") "$container.bar should be bar" + assert ($container.baz -eq "baz") "$container.baz should be baz" + assert ($foo -eq 1) "$foo should be 1" + assert ($bar -eq 2) "$bar should be 2" + assert ($baz -eq 3) "$baz should be 3" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_parameters_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_parameters_should_pass.ps1 new file mode 100644 index 0000000..a1ed57c --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_parameters_should_pass.ps1 @@ -0,0 +1,9 @@ +properties { + $my_property = $p1 + $p2 +} + +task default -depends TestParams + +task TestParams { + assert ($my_property -ne $null) '$my_property should not be null' +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_postcondition_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_postcondition_should_pass.ps1 new file mode 100644 index 0000000..8b40ca8 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_postcondition_should_pass.ps1 @@ -0,0 +1,13 @@ +task default -depends A, B, C + +task A { + "TaskA" +} + +task B -postcondition { return $true } { + "TaskB" +} + +task C { + "TaskC" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_precondition_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_precondition_should_pass.ps1 new file mode 100644 index 0000000..51295ef --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_precondition_should_pass.ps1 @@ -0,0 +1,13 @@ +task default -depends A, B, C + +task A { + "TaskA" +} + +task B -precondition { return $false } { + "TaskB" +} + +task C -precondition { return $true } { + "TaskC" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_properties_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_properties_should_pass.ps1 new file mode 100644 index 0000000..aec8bf6 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_properties_should_pass.ps1 @@ -0,0 +1,13 @@ +properties { + $x = $null + $y = $null + $z = $null +} + +task default -depends TestProperties + +task TestProperties { + assert ($x -ne $null) "x should not be null" + assert ($y -ne $null) "y should not be null" + assert ($z -eq $null) "z should be null" +} \ No newline at end of file diff --git a/Source/Builder.Tests/UnitTest/specs/using_required_when_not_set_should_fail.ps1 b/Source/Builder.Tests/UnitTest/specs/using_required_when_not_set_should_fail.ps1 new file mode 100644 index 0000000..b5dd725 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_required_when_not_set_should_fail.ps1 @@ -0,0 +1,10 @@ +properties { + $x = $null + $y = $null + $z = $null +} + +task default -depends TestProperties + +task TestProperties -requiredVariables z { +} diff --git a/Source/Builder.Tests/UnitTest/specs/using_required_when_set_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/using_required_when_set_should_pass.ps1 new file mode 100644 index 0000000..20b7ad3 --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/using_required_when_set_should_pass.ps1 @@ -0,0 +1,9 @@ +properties { + $x = $null + $y = $null +} + +task default -depends TestRequired + +task TestRequired -requiredVariables x, y { +} diff --git a/Source/Builder.Tests/UnitTest/specs/whatif_preference_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/whatif_preference_should_pass.ps1 new file mode 100644 index 0000000..0f2c94a --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/whatif_preference_should_pass.ps1 @@ -0,0 +1,18 @@ +task default -depends RunWhatIf + +task RunWhatIf { + try + { + # Setup the -whatif flag globally + $global:WhatIfPreference = $true + + # Ensure that the nested script does something with -whatif e.g. Set-Item + $parameters = @{ p1 = 'whatifcheck' } + + Invoke-Builder .\nested\whatifpreference.ps1 -Parameters $parameters + } + finally + { + $global:WhatIfPreference = $false + } +} diff --git a/Source/Builder.Tests/UnitTest/specs/writing_builder_variables_should_pass.ps1 b/Source/Builder.Tests/UnitTest/specs/writing_builder_variables_should_pass.ps1 new file mode 100644 index 0000000..4c6abae --- /dev/null +++ b/Source/Builder.Tests/UnitTest/specs/writing_builder_variables_should_pass.ps1 @@ -0,0 +1,34 @@ +properties { + $x = 1 +} + +task default -depends Verify + +task Verify -description "This task verifies Builder's variables" { + #Verify the exported module variables + cd variable: + assert (Test-Path "BuildEnv") "variable 'BuildEnv' was not exported from module!" + + assert ($BuildEnv.ContainsKey("BuildSuccess")) "'BuildEnv' variable does not contain key 'BuildSuccess'" + assert ($BuildEnv.ContainsKey("Version")) "'BuildEnv' variable does not contain key 'Version'" + assert ($BuildEnv.ContainsKey("BuildScriptFile")) "'BuildEnv' variable does not contain key 'BuildScriptFile'" + assert ($BuildEnv.ContainsKey("BuildScriptDir")) "'BuildEnv' variable does not contain key 'BuildScriptDir'" + + assert (-not $BuildEnv.BuildSuccess) '$BuildEnv.BuildSuccess should be $false' + assert ($BuildEnv.Version) '$BuildEnv.Version was null or empty' + assert ($BuildEnv.BuildScriptFile) '$BuildEnv.BuildScriptFile was null' + assert ($BuildEnv.BuildScriptFile.Name -eq "writing_builder_variables_should_pass.ps1") '$BuildEnv.BuildScriptFile.Name was not equal to "writing_builder_variables_should_pass.ps1"' + assert ($BuildEnv.BuildScriptDir) '$BuildEnv.BuildScriptDir was null or empty' + + assert ($BuildEnv.Context.Count -eq 1) '$BuildEnv.Context should have had a length of one (1) during script execution' + + $config = $BuildEnv.Context.Peek().Setting + assert ($config) '$BuildEnv.Setting is $null' + assert ((New-Object "System.IO.FileInfo" $config.BuildFileName).FullName -eq $BuildEnv.BuildScriptFile.FullName) ('$BuildEnv.Context.Peek().Setting.BuildFileName not equal to "{0}"' -f $BuildEnv.BuildScriptFile.FullName) + assert ($config.EnvPath -eq $null) '$BuildEnv.Context.Peek().Setting.EnvPath is not $null' + assert ($config.TaskNameFormat -eq "Executing {0}") '$BuildEnv.Context.Peek().Setting.TaskNameFormat not equal to "Executing {0}"' + assert (-not $config.VerboseError) '$BuildEnv.Context.Peek().Setting.VerboseError should be $false' + assert ($config.VerboseLevel -is [Int]) '$BuildEnv.Context.Peek().Setting.VerboseLevel should be integer' + assert ($config.ColoredOutput) '$BuildEnv.Context.Peek().Setting.ColoredOutput should be $false' + assert ($config.Modules -eq $null) '$BuildEnv.Context.Peek().Setting.Modules is not $null' +} \ No newline at end of file diff --git a/Source/Builder/Builder-Config.ps1 b/Source/Builder/Builder-Config.ps1 new file mode 100644 index 0000000..84630e5 --- /dev/null +++ b/Source/Builder/Builder-Config.ps1 @@ -0,0 +1,29 @@ +<# +------------------------------------------------------------------- +[Defaults] +These are the default configuration values. You don't have to +specify them. +------------------------------------------------------------------- +$config.BuildFileName = "default.ps1" +$config.EnvPath = $null +$config.TaskNameFormat = "Executing {0}" +$config.VerboseError = $false +$config.ColoredOutput = $true +$config.Modules = $null + +------------------------------------------------------------------- +[Modules] +You can load your custom PowerShell modules into your build session. +Here we load all modules from the 'modules' folder and from file +'my_module.psm1' (all relative to the build script path). +------------------------------------------------------------------- +$config.Modules = ("./modules/*.psm1", "./my_module.psm1") + +------------------------------------------------------------------- +[Print Task Name] +Custom print the task name by overriding 'TaskNameFormat' with a +scriptblock. +------------------------------------------------------------------- +$config.TaskNameFormat = { param($taskName) "Executing $taskName at $(Get-Date)" } + +#> diff --git a/Source/Builder/Builder.ps1 b/Source/Builder/Builder.ps1 new file mode 100644 index 0000000..74f549b --- /dev/null +++ b/Source/Builder/Builder.ps1 @@ -0,0 +1,84 @@ +<# + .SYNOPSIS + This is a helper script for running Builder without importing the module. + + .EXAMPLE + .\builder.ps1 "buildscript.ps1" "BuildHelloWord" "netfx462" + + DESCRIPTION + ----------- + You need to match parameter definitions for Builder.psm1/Invoke-Builder. Otherwise named parameter binding fails. +#> +[CmdletBinding()] +Param( + [Parameter(Position = 1, Mandatory = $false)] + [string]$BuildFile, + + [Parameter(Mandatory = $false, Position = 2)] + [string[]]$TaskList = @(), + + [Parameter(Mandatory = $false)] + [switch]$Docs, + + [Parameter(Mandatory = $false)] + [hashtable]$Parameters = @{}, + + [Parameter(Mandatory = $false)] + [hashtable]$Properties = @{}, + + [Parameter(Mandatory = $false)] + [Alias("Init")] + [scriptblock]$Initialization = {}, + + [Parameter(Mandatory = $false)] + [switch]$NoLogo, + + [Parameter(Mandatory = $false)] + [switch]$Help, + + [Parameter(Mandatory = $false)] + [string]$ScriptPath, + + [Parameter(Mandatory = $false)] + [switch]$DetailDocs, + + [Parameter(Mandatory = $false)] + [switch]$TimeReport +) + +# setting $scriptPath here, not as default argument, to support calling as "powershell -File Builder.ps1" +if (-not $ScriptPath) +{ + $ScriptPath = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +# '[B]uilder' is the same as 'builder' but $Error is not polluted +Remove-Module [B]uilder +Import-Module (Join-Path $ScriptPath -ChildPath 'Builder.psm1') -Verbose:$false +if ($Help) +{ + Get-Help Invoke-Builder -Full + return +} + +if ($BuildFile -and (-not (Test-Path $BuildFile -PathType Leaf))) +{ + $buildFileFullPath = Join-Path $ScriptPath -ChildPath $BuildFile + if (Test-Path $buildFileFullPath -PathType Leaf) + { + $BuildFile = $buildFileFullPath + } +} + +$buildParams = @{ + 'BuildFile' = $BuildFile + 'TaskList' = $TaskList + 'Docs' = $Docs + 'Parameters' = $Parameters + 'Properties' = $Properties + 'Initialization' = $Initialization + 'NoLogo' = $NoLogo + 'DetailDocs' = $DetailDocs + 'TimeReport' = $TimeReport +} +Invoke-Builder @buildParams diff --git a/Source/Builder/Builder.psd1.pstmpl b/Source/Builder/Builder.psd1.pstmpl new file mode 100644 index 0000000000000000000000000000000000000000..1e2b0e39f8c955f768e02125dbaa0608b25d813d GIT binary patch literal 13774 zcmd6t+ioMr5r+FZK;B_s8K440QXD&p5gkH zb+NX)hciRcLmCT0;$gb0>-bl7b^q_bTcHYDAI;fr3ZH65`u$Y1?<1Wb!;fJQYz<$@XIyEXY;+T=v8b>N`-anL-$kCB@A|5h<_*I?r8Bs>(*NBW?;n?yk>k&@7Vp*UwWar0 z;vE}2L}_z7R?-bNztq#Q^!O-i;Ty@)N$-`OKj}3QwNcc$*#&*0Q`4+jvhH1`H|Ob0 zG$xv3R&y&#ut!uBA+QaO|q6;=G@h3 zw)0vanc}0)=W#d`C(q*4^f(o7d?zZ+=kB64M)H3AZ>ryK;xB01^_;lR>3 z^j7qtS1AfTZu5C~%f802f_X-lzVeynta8T}}eo9&i zuS(89sI+1l)y-oa)4)%|DKHUBB{W@ZD>$uFoaeGWqMgG-@$i&IyvISZB&c{3J?$!b z6}WgIoFtCX`-38765d2lG2gsg((l^VF6F_Wn>cKj>6N&B&(D={H!YcM>6y5K{~jIIA4Vw=>%jVj|^h|9O%-gXJLL0e8w)E^} z966HLgNl``T%u*^B|pT`#NJz3tdjQl#Ee%li<*A$`P(=W&nl$?YnJt%ChB!OEj_7R z3)|MmxBFCz_Maj%_-J)g$-^r0HhbvTGs=m-zW&*y*ZEbtG&?`KU9X$)u+3|`?l+s% zd3~*AG+&?7SN~Xvst_;6|G}2sa4JMS*QX;ru?qjKo<4?u^or|dKR*=Zsa727e_gu1 z+Rtgv%TiyhOaD2g`n<(lelDHL z4#G(|i<#o5BbCvp%ipE91M&7aXV-SG{Z^ydSx_0^k<{w!N3LS6OU|cOdDbsTxh=mz z1H+i_{uaIrzmK2xCH1vFf6-Ikd2aSwdC9dvg${S0&T5INz9Y+h;meV5cLO!4(LVj{Nehi+tR@4Px+_)}jC zBwCBL-^IgvHGdTIg6~&9F74;Mt-}H(z16Hx&fP=|9LaC#gi_mk=2*p-cQucW4ta+9 zEARSl$Jzqa8>qUR@8=E_TO}ru8}fYGrY-s^d5xTZuG6q=N&My!YMp92zvNTz7v&Ma zymHr{Kc(*vb|5kSYdtVS^(I;O7RZ%L;K-cwmTRRyDzKw+sdLK6T;|qp9&U;_?%Q`d ziu;n(Z%mfs6vnGk`jX@jI#Y`~r>@JKLVcb7`m9s5T=$~jy434pe%7no1L^BJRv1%R z-YfE)i~f4e*XXa)oZ4#o%x7)_Grw20#@^;4R^(ofgD~sWXMLUiI;~S&O|SLYo6zmP zd=$;H&)t-trZxI&G|_@z^3}3YpJr}CkB91WAIKxg%JfR;idg4lGqHcDRj#yN-+I;P zunR1?UPZGxFY86o6ZxjqN>&+M$Xn@wzk3HSx9=t^*^X)w~4?7V(-bcSF zWdQHm*7-qmtJtxHQ?o4Rt$g-d9`q*YBH3LLr;T@RbWMeaPvMVYPcP0U#(G(=epj}spE2#mG4~qVbSAl{>VK%$ zb?iTbv3t>m*cYuo>H8Pqx9Y!r89onxXw+%xU0uGITl;+0{br}jPLdiB@3OmPLwOy& z?os%cp7?Epd0vvTK~PW8?u|vkndJXI}hz%R>9$aD)H%q`V zZk_O%n0Bo~!DF2|5le6eRQ3{_OMgGtIOD9uX*|b%CnKy6pM70_-x78AA)@XwN&x~{ zjfPZSRaigKVujiP9U-B48&9nlnQn#|e&xvLy>i0~*1&VY8Dg1!53xM&l}(ZCG91U) z9P8#7o?iDOFzG5jy9`jv#j$h%ei{b( z*`!ASn&9pjymGILZ;Ng}<*3yaIVC5YOgkSX7PrUHnPflF-+t7>Y4}EH@tsZ@&QTgrX_AYm~&W>eE&=6{6oRvOp zd8a-m$?ur&?{@pYDVpqNW?ql7Qrk|#>1}Dui7aP3cd*}f0>j4Y&}aFK@3QfAtAi0x zEypCunq+YL##N4sR3F@^qKCw{W9Ap-w|q4;`gQcg2W5jcRdwtoF ztKgJVGwe;Kp&GL&p)!7_Z@#1SyIfZ968U+L+4ioB&O^xl-^_0DQn#_^O-RbOc0RDO z*ylpGIgzlRafVa$iZjq|Nj9I CTWS6P literal 0 HcmV?d00001 diff --git a/Source/Builder/Builder.psm1 b/Source/Builder/Builder.psm1 new file mode 100644 index 0000000..7c8145f --- /dev/null +++ b/Source/Builder/Builder.psm1 @@ -0,0 +1,1760 @@ +#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 +{ + <# + .SYNOPSIS + Helper command for executing command-line programs. + + .DESCRIPTION + Define the script block to call an external program. This command automatically checks the `$lastexitcode` variable to + determine whether an error has occcured. + + If an error is detected, the default behavior is to throw an exception and terminate. Alternatively, you may re-execute + the script block several times until there are no errors. + + .PARAMETER Command + The script block to execute. This script block will typically contain the command-line invocation. + + .PARAMETER ErrorMessage + The error message to display if an exception is thrown. + + .PARAMETER MaxRetry + Repeat execution of the script block if an error was encountered, up to the number of times defined by this parameter. + + .PARAMETER RetryDelay + When re-executing the script block, wait for the number of seconds defined by this parameter between each attempt. + + .PARAMETER RetryTriggerErrorPattern + Re-execute the script block only if the last error message matches the regular expression pattern defined by this + parameter. + + .PARAMETER NoWill + Do not trigger any last wills (if defined) when failing. + #> + + [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 +{ + <# + .SYNOPSIS + A helper keyword for "Design by Contract" assertion checking. + + .DESCRIPTION + Assertions helps to make the code less "noisy" by eliminating the need to write nested + `if` statements that are normally required to verify assumptions in the code. + + .PARAMETER Condition + The boolean condition to evaluate. + + .PARAMETER ErrorMessage + The error message to display if the `Condition` parameter is false. + + .PARAMETER NoWill + Do not trigger any last wills (if defined) when failing. + + .EXAMPLE + assert $false "This always throws an exception" + + .EXAMPLE + assert (($i % 2) -eq 0) "$i is not an even number" + + DESCRIPTION + ----------- + This statement may throw an exception if `$i` is not an even number. + + Note: you may need to wrap the condition with paranthesis to prevent a syntax error. + #> + + [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 +{ + <# + .SYNOPSIS + Define a script block that contains assignments to variables. These variables will be available to all tasks in the build script. + + .DESCRIPTION + A build script may use the `properties` keyword to define variables. These variables will be available to all the `tasks` in the build script. + + .PARAMETER Properties + The script block containing all the variable assignment statements. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [scriptblock]$ScriptBlock + ) + + $BuildEnv.Context.Peek().Properties += $ScriptBlock +} + +function Will +{ + <# + .SYNOPSIS + Execute a script block whenever an exception is encountered. + + .DESCRIPTION + Use this keyword to define a last will script block. Last wills are executed + just before the build script terminates due to an exception. + + .PARAMETER ScriptBlock + The last will script block to execute. + + .NOTES + You can define multiple last wills by using the `will` keyword repeatedly. The wills + will be executed in the order that they are defined. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [scriptblock]$ScriptBlock + ) + + $BuildEnv.Context.Peek().Will += $ScriptBlock +} + +function PrintTask +{ + <# + .SYNOPSIS + Customize how to render the task name during a build. + + .DESCRIPTION + Accepts either a string which represents a format string (formats using the -f format operator see "help about_operators"), or + a script block that has a single parameter that is the name of the task that will be executed. + + .PARAMETER Format + A format string or a script block to execute. + + .EXAMPLE + task default -depends TaskA, TaskB, TaskC + + printTask "-------- {0} --------" + + task TaskA { + "TaskA is executing" + } + + task TaskB { + "TaskB is executing" + } + + task TaskC { + "TaskC is executing" + } + + # The script above produces the following output: + # + # -------- TaskA -------- + # TaskA is executing + # -------- TaskB -------- + # TaskB is executing + # -------- TaskC -------- + # TaskC is executing + # + # Build Succeeded! + + DESCRIPTION + ----------- + Use a format string to customize how the task name is printed. + + .EXAMPLE + printTask { + param($taskName) + + say "Executing Task: $taskName" -fg blue + } + + task default -depends TaskA, TaskB, TaskC + + task TaskA { + "TaskA is executing" + } + + task TaskB { + "TaskB is executing" + } + + task TaskC { + "TaskC is executing" + } + + DESCRIPTION + ----------- + Use a script block to customize how the task name is printed. + + This example uses the script block parameter to the `printTask` keyword to render each + task name in the color blue. + + Note: the `$taskName` parameter is arbitrary it could be named anything. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + $Format + ) + + $BuildEnv.Context.Peek().Setting.TaskNameFormat = $Format +} + +function Include +{ + <# + .SYNOPSIS + Include the functions or code of another script file into the current build script's scope. + + .DESCRIPTION + A build script may declare an "include" keyword which allows you to define a script to be + included and added to the scope of the currently running build script. Code from such file + will be executed AFTER code from build script. + + .PARAMETER FilePath + A string containing the path of the script file to include. + #> + + [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 +{ + <# + .SYNOPSIS + Defines a script block that will be executed before each task. + + .DESCRIPTION + Use this keyword to define a script block that will be executed before each + task in the build script. + + .PARAMETER ScriptBlock + A script block to execute. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [scriptblock]$ScriptBlock + ) + + $BuildEnv.Context.Peek().TaskSetupScriptBlock = $ScriptBlock +} + +function TaskTearDown +{ + <# + .SYNOPSIS + Runs a script block after each task. + + .DESCRIPTION + Use this keyword to define a script block that will be executed after each + task in the build script. + + .PARAMETER ScriptBlock + A scriptblock to execute. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [scriptblock]$ScriptBlock + ) + + $BuildEnv.Context.Peek().TaskTearDownScriptBlock = $ScriptBlock +} + +function EnvPath +{ + <# + .SYNOPSIS + Sets the environmental paths you want to use during build. + + .DESCRIPTION + This keyword accept a list of directory paths. These paths will be prepended to the existing system paths within the calling context. + + .PARAMETER Path + A list of paths to prepend to the existing system paths. + #> + + [CmdletBinding()] + Param( + [Parameter(Position = 1, Mandatory = $true)] + [string[]]$Path + ) + + $BuildEnv.Context.Peek().Setting.EnvPath = $Path + ConfigureBuildEnvironment +} + +function Invoke-Task +{ + <# + .SYNOPSIS + Executes another task in the current build script. + + .DESCRIPTION + This keyword allows you to invoke a task from within another task in the current build script. + + .PARAMETER TaskName + The name of the task to execute. + #> + + [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 +{ + <# + .SYNOPSIS + Defines a build task to be executed by Builder. + + .DESCRIPTION + Use within a build script. This keyword creates a 'task' object that will be used by the Builder engine to execute + a build task. + + NOTE: You must defined a task called 'default'. + + .PARAMETER Action + A script block containing the statements to execute for the task. + + .PARAMETER ContinueOnError + If this switch parameter is set then the task will not cause the build to fail when an error occurs while running the task. + + .PARAMETER Depends + An array of task names that this task depends on. These tasks will be executed before the current task is executed. + + .PARAMETER Description + A description of the task for documentation purposes. + + .PARAMETER Name + The name of the task. + + .PARAMETER Alias + An alternative name for the task. + + .PARAMETER PostAction + A script block to be executed after the `Action` scriptblock. + + NOTE: This parameter is silently ignored if the `Action` script block is undefined. + + .PARAMETER Postcondition + A script block that is executed to determine if the task completed its job correctly. + + An exception is thrown if the script block returns `$false`. + + .PARAMETER PreAction + A scriptblock to be executed before the `Action` script block. + + NOTE: This parameter is silently ignored if the `Action` script block is undefined. + + .PARAMETER Precondition + A script block that is executed to determine whether the task should be is executed or skipped. + + This script block should return either `$true` or `$false`. + + .PARAMETER RequiredVariables + An array of names of variables that must be set to run this task. + #> + + [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 +{ + <# + .SYNOPSIS + Prints a text output. + + .DESCRIPTION + Use this command within a build script to print a text output. + + .PARAMETER Message + The text to print. + + .PARAMETER Divider + Prints text that represents a dividing line: + + ++++++++ + + .PARAMETER NewLine + Prints an empty line (line break). + + .PARAMETER LineCount + Use together with the `NewLine` parameter to output multiple line breaks. + + .PARAMETER VerboseLevel + Defines the verbose level of the output text. If the verbose level defined is higher than + the output verbose level setting, the text is ignored (unless the `Force` parameter is used). + + .PARAMETER ForegroundColor + Specifies the color of the output text. This parameter is silently ignored if the output medium + does not support color output. + + .PARAMETER Force + Ensures that the text is displayed, regardless of its verbose level. + #> + + [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 +{ + <# + .SYNOPSIS + Terminates the build script with an error message. + + .DESCRIPTION + Use this command in a build script to raise a terminating exception. + + .PARAMETER Message + The error message to display. + + .PARAMETER ErrorCode + The error code to return. + + .PARAMETER NoWill + If one or more `will` block is present in the build script, these script blocks will + be executed in order before the terminating exception is raised. This parameter + overrides this behavior and terminates the build script immediately. + + If no `will` block is defined, this parameter does not have any effect. + #> + + [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 +{ + <# + .SYNOPSIS + Returns metadata on tasks. + + .DESCRIPTION + This command parses the build script specified and returns metadata about all the tasks defined. + + .PARAMETER BuildFile + The path to the build script to evaluate tasks metadata. + #> + + [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 +{ + <# + .SYNOPSIS + Runs a build script. + + .DESCRIPTION + Use this command to execute a build script. Refer to the links section for examples on how to write build scripts using Builder DSL syntax. + + .PARAMETER BuildFile + The path to the build script to execute. + + .PARAMETER Docs + Prints a list of tasks. This parameter is valid but redundant if the `DetailDocs` parameter is also specified. + + .PARAMETER DetailDocs + Prints a list of tasks and their descriptions. + + .PARAMETER Initialization + Runs a script before starting the build script. + + .PARAMETER NoLogo + Do not display the startup banner and copyright message. + + .PARAMETER Parameters + A hashtable containing parameters to be passed into the current build script. These parameters will be processed before the `Properties` function of the + script is processed. This means you can access parameters from within the `Properties` function. + + .PARAMETER Properties + A hashtable containing properties to be passed into the current build script. These properties will override matching properties that are found in the + `Properties` function of the script. + + .PARAMETER TaskList + A comma-separated list of task names to execute. + + .PARAMETER TimeReport + Display the time report. + + .EXAMPLE + Invoke-Builder + + DESCRIPTION + ----------- + Runs the 'default' task in the '.build.ps1' build script. + + + + .EXAMPLE + Invoke-Builder '.\build.ps1' Tests,Package + + DESCRIPTION + ----------- + Runs the 'Tests' and 'Package' tasks in the '.build.ps1' build script. + + + + .EXAMPLE + Invoke-Builder Tests + + DESCRIPTION + ----------- + Run the 'Tests' task in the 'default.ps1' build script. The 'default.ps1' file is assumed to be in the current directory. + + + + .EXAMPLE + Invoke-Builder 'Tests, Package' + + DESCRIPTION + ----------- + Run the 'Tests' and 'Package' tasks in the 'default.ps1' build script. The 'default.ps1' file is assumed to be in the current directory. + + NOTE: The quotes around the list of tasks to execute is required if you want to execute more than 1 task. + + + + .EXAMPLE + Invoke-Builder .\build.ps1 -docs + + DESCRIPTION + ----------- + Prints a report of all the tasks and their dependencies and descriptions and then exits. + + + + .EXAMPLE + @' + properties { + $my_property = $p1 + $p2 + } + + task default -depends TestParams + + task TestParams { + assert ($my_property -ne $null) '$my_property should not be null' + } + '@ | Set-Content -Path .\parameters.ps1 + Invoke-Builder .\parameters.ps1 -Parameters @{ "p1" = "v1"; "p2" = "v2" } + + DESCRIPTION + ----------- + Runs the build script called 'parameters.ps1' and passes in parameters 'p1' and 'p2' with values 'v1' and 'v2'. + + Notice how you can refer to the parameters that were passed into the script from within the "properties" function. The value of + the `$p1` variable should be the string "v1" and the value of the `$p2` variable should be "v2". + + + + .EXAMPLE + @' + properties { + $x = $null + $y = $null + $z = $null + } + + task default -depends TestProperties + + task TestProperties { + assert ($x -ne $null) "x should not be null" + assert ($y -ne $null) "y should not be null" + assert ($z -eq $null) "z should be null" + } + '@ | Set-Content -Path .\properties.ps1 + Invoke-Builder .\properties.ps1 -Properties @{ "x" = "1"; "y" = "2" } + + DESCRIPTION + ----------- + Runs the build script called 'properties.ps1' and passes in parameters 'x' and 'y' with values '1' and '2'. + + This feature allows you to override existing properties in your build script. + + + + .NOTE + ---- Exceptions ---- + + If there is an exception thrown during the running of a build script Builder will set the `$BuildEnv.BuildSuccess` variable to + `$false`. To detect failue outside PowerShell (for example by build server), finish PowerShell process with non-zero exit code when + `$BuildEnv.BuildSuccess` is `$false`. Calling Builder from 'cmd.exe' with 'build.cmd' will give you that behaviour. + + + + ---- BuildEnv variable ---- + + When the Builder module is loaded, a special variable called `$BuildEnv` is created. This variable is a hashtable containing the + following keys: + + - **Version**: contains the current version of Builder + - **Context**: holds onto the current state of all variables + - **RunByUnitTest**: indicates that build is being run by the unit tester. Do not modify this variable. + - **DefaultSetting**: contains default configuration. To override, modify the 'Builder-Config.ps1' file, which + must be placed in the same directory as 'Builder.psm1' or the build script. + - **BuildSuccess**: indicates that the current build was successful. + - **BuildScriptFile**: contains a `System.IO.FileInfo` object representing the current build script. + - **BuildScriptDir**: contains the fully qualified path to the current build script. + + .LINK + http://www.github.com/buildcenter/Builder + + .LINK + Task + + .LINK + Include + + .LINK + Properties + + .LINK + PrintTask + + .LINK + TaskSetup + + .LINK + TaskTearDown + + .LINK + Assert + + .LINK + EnvPath + #> + + [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' +) diff --git a/Source/Builder/build.cmd b/Source/Builder/build.cmd new file mode 100644 index 0000000..31844de --- /dev/null +++ b/Source/Builder/build.cmd @@ -0,0 +1,14 @@ +@echo off +rem Helper script for those who want to run Builder from cmd.exe +rem Example run from cmd.exe: +rem build "default.ps1" "BuildHelloWord" "4.0" + +if '%1'=='/?' goto help +if '%1'=='-help' goto help +if '%1'=='-h' goto help + +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0\Builder.ps1' %*; if ($BuildEnv.BuildSuccess -eq $false) { exit 1 } else { exit 0 }" +exit /B %errorlevel% + +:help +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0\Builder.ps1' -Help" diff --git a/Source/Builder/en-US/Message.psd1 b/Source/Builder/en-US/Message.psd1 new file mode 100644 index 0000000..100517e --- /dev/null +++ b/Source/Builder/en-US/Message.psd1 @@ -0,0 +1,38 @@ +# Localized 24/5/2017 2:06 PM (GMT) 410:2.92.0533 Message.psd1 +# Builder PBLocalizedData.en-US + +ConvertFrom-StringData @' + +# ---- [ Localized Data ] --------------------------------------------- + +Err_InvalidTaskName = Task name cannot be null or empty string. +Err_TaskNameDoesNotExist = Task does not exist: {0} +Err_CircularReference = The task has circular references: {0} +Err_MissingActionParameter = Action parameter must be specified when using PreAction or PostAction parameters: {0} +Err_CorruptCallStack = Call stack was corrupt (expected '{0}', but got '{1}'). +Err_EnvPathDirNotFound = The environmental path specified does not exist, or is not a filesystem directory: {0} +Err_BadCommand = Error executing command: {0} +Err_DefaultTaskCannotHaveAction = Do not specify an action for the 'default' task. +Er_DuplicateTaskName = Task has already been defined: {0} +Err_DuplicateAliasName = Alias has already been defined: {0} +Err_InvalidIncludePath = Unable to include a file because it was not found: {0} +Err_BuildFileNotFound = Unable to find build file: {0} +Err_NoDefaultTask = A 'default' task is required. +Err_LoadingModule = Error loading module: {0} +Err_LoadConfig = Error loading build configuration: {0} +RequiredVarNotSet = Variable '{0}' must be set to run task '{1}'. +PostconditionFailed = Postcondition failed for task: {0} +PreconditionWasFalse = Precondition was false, not executing task: {0} +ContinueOnError = Error in task '{0}': {1} +BuildSuccess = Build succeeded! +RetryMessage = Attempt {0}/{1} failed, retrying in {2} second... +BuildTimeReportTitle = Build Time Report +Divider = ---------------------------------------------------------------------- +ErrorHeaderText = An error has occured. See details below: +ErrorLabel = Error +VariableLabel = Script variables: +DefaultTaskNameFormat = Executing {0} +UnknownError = An unknown error has occured. + +# ---- [ /Localized Data ] -------------------------------------------- +'@ diff --git a/Source/Builder/en-US/about_Builder.help.txt.pstmpl b/Source/Builder/en-US/about_Builder.help.txt.pstmpl new file mode 100644 index 0000000..c5e7f96 --- /dev/null +++ b/Source/Builder/en-US/about_Builder.help.txt.pstmpl @@ -0,0 +1,58 @@ +TOPIC + about_{{ $moduleName }} + +SYNOPSIS + {{ $synopsis }} + +VERSION + You are running version {{ $moduleVersion }} of {{ $moduleName }} :) + +SHORT DESCRIPTION + {{ $description }} + +INSTALLATION + The best option (if you are on Windows 10 or similar) is to use the `Install-Package` command from + PowerShell. + + # Using Install-Package + + 1. `Install-Package {{ $moduleName }}` + + 2. `ipmo {{ $moduleName }}` + + 3. `Get-Help about_{{ $moduleName }}`, or `Get-Command -Module {{ $moduleName }}`. + + 4. YOUR WORK HERE IS DONE. + + + # Manual portable installation + + If that doesn't work for you, try installing it manually. + + 1. Download the latest release ({{ $downloadBaseUrl }}{{ $moduleName | lowercase }}.zip)[from here]. If you want a specific + version, it's something like `{{ $downloadBaseUrl }}{{ $moduleName | lowercase }}.{{ $moduleVersion }}.zip`. + + 2. Copy the '{{ $moduleName }}' folder in archive to `Documents\WindowsPowerShell\Modules`. + So you get `Documents\WindowsPowerShell\Modules\{{ $moduleName }}\{{ $moduleName }}.psd1`, ... + + 3. Make sure your PowerShell policy allows running scripts (in an escalated terminal, type in: + `Set-ExecutionPolicy Unrestricted`). + + 4. Here is a short script snipplet to automate the steps above: + ``` + wget {{ $downloadBaseUrl }}{{ $moduleName | lowercase }}.zip -OutFile $env:TEMP\{{ $moduleName | lowercase }}.zip + Expand-Archive $env:TEMP\{{ $moduleName | lowercase }}.zip $env:PSModulePath.Split(';')[0] + ipmo {{ $moduleName }} + ``` + +CHANGELOG + {{ $docUrlBase }}CHANGELOG.md + +LINKS + {{ $projectUrl }} + +COPYRIGHT + {{ $copyright }} + +LICENSE +{{ $repoDir | concat '/LICENSE.txt' | include -indent ' ' }} diff --git a/Source/Builder/zh-CN/Builder-Help_Assert.md b/Source/Builder/zh-CN/Builder-Help_Assert.md new file mode 100644 index 0000000..2aa7d42 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Assert.md @@ -0,0 +1,26 @@ +.SYNOPSIS + 一个帮助实施“合同式设计”的断言检测帮助工具。 + +.DESCRIPTION + 在核实前提假设时,利用断言取代代码中的多层`if`声明,可以使代码更加精简易懂。 + +.PARAMETER Condition + 需要判断的布尔条件。若判断为“false”则引发错误提示。 + +.PARAMETER ErrorMessage + 自定义`Condition`参数判断为“false”时报错的文字内容。 + +.PARAMETER NoWill + 报错时不要引发任何遗嘱(如有)。 + +.EXAMPLE + assert $false "This always throws an exception" + +.EXAMPLE + assert (($i % 2) -eq 0) "$i is not an even number" + + DESCRIPTION + ----------- + 如果`$i`不是偶数,这个声明就会引发错误提示。 + + 注:为避免语法错误,你可能需要将条件用括弧包起来。 diff --git a/Source/Builder/zh-CN/Builder-Help_Die.md b/Source/Builder/zh-CN/Builder-Help_Die.md new file mode 100644 index 0000000..58493d8 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Die.md @@ -0,0 +1,17 @@ +.SYNOPSIS + 终止搭建脚本的执行,并引发错误提示。 + +.DESCRIPTION + 在搭建脚本里使用这个命令,可以引发错误提示,并终止执行脚本。 + +.PARAMETER Message + 报错的文字内容。 + +.PARAMETER ErrorCode + 报错的错误代码。 + +.PARAMETER NoWill + 如果搭建脚本中定义了一个或多个`will`脚本块,这些脚本块将按顺序执行,全部执行完毕后才会引发错误提示并终止运行。你可以使用这个参数覆盖上述行为,以 + 实现立即终止脚本的执行。 + + 如果脚本中未定义任何`will`脚本块,该参数则不会产生任何影响。 diff --git a/Source/Builder/zh-CN/Builder-Help_EnvPath.md b/Source/Builder/zh-CN/Builder-Help_EnvPath.md new file mode 100644 index 0000000..abdbbfe --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_EnvPath.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 在脚本的变量范围内设置环境路径。 + +.DESCRIPTION + 这个命令可以接受一个文件系统路径的数组。这些路径将会在调用上下文内前置于现有的路径。 + +.PARAMETER Path + 一个文件系统路径的数组。这些路径将前置于现有的系统路径。 diff --git a/Source/Builder/zh-CN/Builder-Help_Exec.md b/Source/Builder/zh-CN/Builder-Help_Exec.md new file mode 100644 index 0000000..7b36066 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Exec.md @@ -0,0 +1,28 @@ +.SYNOPSIS + Helper command for executing command-line programs. + +.DESCRIPTION + Define the script block to call an external program. This command automatically checks the `$lastexitcode` variable to + determine whether an error has occcured. + + If an error is detected, the default behavior is to throw an exception and terminate. Alternatively, you may re-execute + the script block several times until there are no errors. + +.PARAMETER Command + The script block to execute. This script block will typically contain the command-line invocation. + +.PARAMETER ErrorMessage + The error message to display if an exception is thrown. + +.PARAMETER MaxRetry + Repeat execution of the script block if an error was encountered, up to the number of times defined by this parameter. + +.PARAMETER RetryDelay + When re-executing the script block, wait for the number of seconds defined by this parameter between each attempt. + +.PARAMETER RetryTriggerErrorPattern + Re-execute the script block only if the last error message matches the regular expression pattern defined by this + parameter. + +.PARAMETER NoWill + Do not trigger any last wills (if defined) when failing. diff --git a/Source/Builder/zh-CN/Builder-Help_Get-BuildScriptTasks.md b/Source/Builder/zh-CN/Builder-Help_Get-BuildScriptTasks.md new file mode 100644 index 0000000..7434f10 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Get-BuildScriptTasks.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 返回关于任务的元数据。 + +.DESCRIPTION + 这个命令可以在解析搭建脚本后,返回脚本定义了的任务的元数据。 + +.PARAMETER BuildFile + 搭建脚本的文件路径。 diff --git a/Source/Builder/zh-CN/Builder-Help_Include.md b/Source/Builder/zh-CN/Builder-Help_Include.md new file mode 100644 index 0000000..7881854 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Include.md @@ -0,0 +1,9 @@ +.SYNOPSIS + 将另一个脚本文件中所定义的函数或代码插入该搭建脚本的范围内。 + +.DESCRIPTION + 在搭建脚本中可以通过"include"命令插入将另外一个脚本文件的内容,并将其代码并入执行范围内。代码将被插入到 + 现有代码的后面。 + +.PARAMETER FilePath + 需插入的脚本文件的路径。 \ No newline at end of file diff --git a/Source/Builder/zh-CN/Builder-Help_Invoke-Builder.md b/Source/Builder/zh-CN/Builder-Help_Invoke-Builder.md new file mode 100644 index 0000000..7d227ce --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Invoke-Builder.md @@ -0,0 +1,180 @@ +.SYNOPSIS + Runs a Builder build script. + +.DESCRIPTION + Use this command to execute a build script. Refer to the links section for examples on how to write build scripts using Builder DSL syntax. + +.PARAMETER BuildFile + The path to the build script to execute. + +.PARAMETER Docs + Prints a list of tasks. This parameter is valid but redundant if the `DetailDocs` parameter is also specified. + +.PARAMETER DetailDocs + Prints a list of tasks and their descriptions. + +.PARAMETER Initialization + Runs a script before starting the build script. + +.PARAMETER NoLogo + Do not display the startup banner and copyright message. + +.PARAMETER Parameters + A hashtable containing parameters to be passed into the current build script. These parameters will be processed before the `Properties` function of the + script is processed. This means you can access parameters from within the `Properties` function. + +.PARAMETER Properties + A hashtable containing properties to be passed into the current build script. These properties will override matching properties that are found in the + `Properties` function of the script. + +.PARAMETER TaskList + A comma-separated list of task names to execute. + +.PARAMETER TimeReport + Display the time report. + +.EXAMPLE + Invoke-Builder + + DESCRIPTION + ----------- + Runs the 'default' task in the '.build.ps1' build script. + + + +.EXAMPLE + Invoke-Builder '.\build.ps1' Tests,Package + + DESCRIPTION + ----------- + Runs the 'Tests' and 'Package' tasks in the '.build.ps1' build script. + + + +.EXAMPLE + Invoke-Builder Tests + + DESCRIPTION + ----------- + Run the 'Tests' task in the 'default.ps1' build script. The 'default.ps1' file is assumed to be in the current directory. + + + +.EXAMPLE + Invoke-Builder 'Tests, Package' + + DESCRIPTION + ----------- + Run the 'Tests' and 'Package' tasks in the 'default.ps1' build script. The 'default.ps1' file is assumed to be in the current directory. + + NOTE: The quotes around the list of tasks to execute is required if you want to execute more than 1 task. + + + +.EXAMPLE + Invoke-Builder .\build.ps1 -docs + + DESCRIPTION + ----------- + Prints a report of all the tasks and their dependencies and descriptions and then exits. + + + +.EXAMPLE + @' + properties { + $my_property = $p1 + $p2 + } + + task default -depends TestParams + + task TestParams { + assert ($my_property -ne $null) '$my_property should not be null' + } + '@ | Set-Content -Path .\parameters.ps1 + Invoke-Builder .\parameters.ps1 -Parameters @{ "p1" = "v1"; "p2" = "v2" } + + DESCRIPTION + ----------- + Runs the build script called 'parameters.ps1' and passes in parameters 'p1' and 'p2' with values 'v1' and 'v2'. + + Notice how you can refer to the parameters that were passed into the script from within the "properties" function. The value of + the `$p1` variable should be the string "v1" and the value of the `$p2` variable should be "v2". + + + +.EXAMPLE + @' + properties { + $x = $null + $y = $null + $z = $null + } + + task default -depends TestProperties + + task TestProperties { + assert ($x -ne $null) "x should not be null" + assert ($y -ne $null) "y should not be null" + assert ($z -eq $null) "z should be null" + } + '@ | Set-Content -Path .\properties.ps1 + Invoke-Builder .\properties.ps1 -Properties @{ "x" = "1"; "y" = "2" } + + DESCRIPTION + ----------- + Runs the build script called 'properties.ps1' and passes in parameters 'x' and 'y' with values '1' and '2'. + + This feature allows you to override existing properties in your build script. + + + +.NOTE + ---- Exceptions ---- + + If there is an exception thrown during the running of a build script Builder will set the `$BuildEnv.BuildSuccess` variable to + `$false`. To detect failue outside PowerShell (for example by build server), finish PowerShell process with non-zero exit code when + `$BuildEnv.BuildSuccess` is `$false`. Calling Builder from 'cmd.exe' with 'Builder.cmd' will give you that behaviour. + + + + ---- BuildEnv variable ---- + + When the Builder module is loaded, a special variable called `$BuildEnv` is created. This variable is a hashtable containing the + following keys: + + - **Version**: contains the current version of Builder + - **Context**: holds onto the current state of all variables + - **RunByUnitTest**: indicates that build is being run by the unit tester. Do not modify this variable. + - **DefaultSetting**: contains default configuration. To override, modify the 'Builder-Config.ps1' file, which + must be placed in the same directory as 'Builder.psm1' or the build script. + - **BuildSuccess**: indicates that the current build was successful. + - **BuildScriptFile**: contains a `System.IO.FileInfo` object representing the current build script. + - **BuildScriptDir**: contains the fully qualified path to the current build script. + +.LINK + http://www.github.com/buildcenter/Builder + +.LINK + Task + +.LINK + Include + +.LINK + Properties + +.LINK + PrintTask + +.LINK + TaskSetup + +.LINK + TaskTearDown + +.LINK + Assert + +.LINK + EnvPath diff --git a/Source/Builder/zh-CN/Builder-Help_Invoke-Task.md b/Source/Builder/zh-CN/Builder-Help_Invoke-Task.md new file mode 100644 index 0000000..329a08d --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Invoke-Task.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 在一个任务中调用另外一个任务。 + +.DESCRIPTION + 这个命令可以让你在一个任务中调用另外一个搭建脚本中已定义的任务。 + +.PARAMETER TaskName + 需调用的任务的名称。 diff --git a/Source/Builder/zh-CN/Builder-Help_PrintTask.md b/Source/Builder/zh-CN/Builder-Help_PrintTask.md new file mode 100644 index 0000000..5a983a5 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_PrintTask.md @@ -0,0 +1,71 @@ +.SYNOPSIS + Customize how to render the task name during a build. + +.DESCRIPTION + Accepts either a string which represents a format string (formats using the -f format operator see "help about_operators"), or + a script block that has a single parameter that is the name of the task that will be executed. + +.PARAMETER Format + A format string or a script block to execute. + +.EXAMPLE + task default -depends TaskA, TaskB, TaskC + + printTask "-------- {0} --------" + + task TaskA { + "TaskA is executing" + } + + task TaskB { + "TaskB is executing" + } + + task TaskC { + "TaskC is executing" + } + + # The script above produces the following output: + # + # -------- TaskA -------- + # TaskA is executing + # -------- TaskB -------- + # TaskB is executing + # -------- TaskC -------- + # TaskC is executing + # + # Build Succeeded! + + DESCRIPTION + ----------- + Use a format string to customize how the task name is printed. + +.EXAMPLE + printTask { + param($taskName) + + say "Executing Task: $taskName" -fg blue + } + + task default -depends TaskA, TaskB, TaskC + + task TaskA { + "TaskA is executing" + } + + task TaskB { + "TaskB is executing" + } + + task TaskC { + "TaskC is executing" + } + + DESCRIPTION + ----------- + Use a script block to customize how the task name is printed. + + This example uses the script block parameter to the `printTask` keyword to render each + task name in the color blue. + + Note: the `$taskName` parameter is arbitrary it could be named anything. diff --git a/Source/Builder/zh-CN/Builder-Help_Properties.md b/Source/Builder/zh-CN/Builder-Help_Properties.md new file mode 100644 index 0000000..55e7fbf --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Properties.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 定义变量赋值的脚本块。搭建脚本的所有任务都可以读取这些变量。 + +.DESCRIPTION + 搭建脚本可以使用`properties`命令来定义变量。这些变量可以在所有任务中被读取。 + +.PARAMETER Properties + 一个包含了所以变量赋值声明的脚本块。 \ No newline at end of file diff --git a/Source/Builder/zh-CN/Builder-Help_Say.md b/Source/Builder/zh-CN/Builder-Help_Say.md new file mode 100644 index 0000000..7634cca --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Say.md @@ -0,0 +1,28 @@ +.SYNOPSIS + 输出自定义的文本。 + +.DESCRIPTION + 在搭建脚本中,可以使用该命令输出文本。 + +.PARAMETER Message + 需输出的文本。 + +.PARAMETER Divider + 输出一段代表分隔符的文字: + + ++++++++ + +.PARAMETER NewLine + 输出一个空行(换行符)。 + +.PARAMETER LineCount + 与`NewLine`参数同时使用,可以输出多个空行。 + +.PARAMETER VerboseLevel + 定义输出文本的详细层级。如果层级高于设定的详细层级,则不会输出该文本(除非同时使用`Force`参数)。 + +.PARAMETER ForegroundColor + 设定输出文本的颜色。如果输出设备不支持颜色,这个参数不会产生任何影响。 + +.PARAMETER Force + 无论详细层级,保证文本必须输出。 diff --git a/Source/Builder/zh-CN/Builder-Help_Task.md b/Source/Builder/zh-CN/Builder-Help_Task.md new file mode 100644 index 0000000..7ca852f --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Task.md @@ -0,0 +1,49 @@ +.SYNOPSIS + Defines a build task to be executed by Builder. + +.DESCRIPTION + Use within a build script. This keyword creates a 'task' object that will be used by the Builder engine to execute + a build task. + + NOTE: You must defined a task called 'default'. + +.PARAMETER Action + A script block containing the statements to execute for the task. + +.PARAMETER ContinueOnError + If this switch parameter is set then the task will not cause the build to fail when an error occurs while running the task. + +.PARAMETER Depends + An array of task names that this task depends on. These tasks will be executed before the current task is executed. + +.PARAMETER Description + A description of the task for documentation purposes. + +.PARAMETER Name + The name of the task. + +.PARAMETER Alias + An alternative name for the task. + +.PARAMETER PostAction + A script block to be executed after the `Action` scriptblock. + + NOTE: This parameter is silently ignored if the `Action` script block is undefined. + +.PARAMETER Postcondition + A script block that is executed to determine if the task completed its job correctly. + + An exception is thrown if the script block returns `$false`. + +.PARAMETER PreAction + A scriptblock to be executed before the `Action` script block. + + NOTE: This parameter is silently ignored if the `Action` script block is undefined. + +.PARAMETER Precondition + A script block that is executed to determine whether the task should be is executed or skipped. + + This script block should return either `$true` or `$false`. + +.PARAMETER RequiredVariables + An array of names of variables that must be set to run this task. diff --git a/Source/Builder/zh-CN/Builder-Help_TaskSetup.md b/Source/Builder/zh-CN/Builder-Help_TaskSetup.md new file mode 100644 index 0000000..9130485 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_TaskSetup.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 在执行每一个任务前先执行该脚本块。 + +.DESCRIPTION + 你可以使用这个命令来定义一个脚本块。这个脚本块会在每一个任务执行前执行。 + +.PARAMETER ScriptBlock + 需定义的脚本块。 \ No newline at end of file diff --git a/Source/Builder/zh-CN/Builder-Help_TaskTearDown.md b/Source/Builder/zh-CN/Builder-Help_TaskTearDown.md new file mode 100644 index 0000000..02ef9c0 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_TaskTearDown.md @@ -0,0 +1,8 @@ +.SYNOPSIS + 在执行每一个任务后执行该脚本块。 + +.DESCRIPTION + 你可以使用这个命令来定义一个脚本块。这个脚本块会在每一个任务执行后执行。 + +.PARAMETER ScriptBlock + 需定义的脚本块。 \ No newline at end of file diff --git a/Source/Builder/zh-CN/Builder-Help_Will.md b/Source/Builder/zh-CN/Builder-Help_Will.md new file mode 100644 index 0000000..24b7e91 --- /dev/null +++ b/Source/Builder/zh-CN/Builder-Help_Will.md @@ -0,0 +1,11 @@ +.SYNOPSIS + 当出现错误时所执行的脚本块。 + +.DESCRIPTION + 使用这个命令定义遗嘱脚本块。当执行脚本时发生了终止性错误时,遗嘱将被执行。遗嘱执行完毕后,才报错并且退出。 + +.PARAMETER ScriptBlock + 发生终止性错误时需执行的遗嘱。 + +.NOTES + 你可以定义多个遗嘱。这些遗嘱会按照所定义的顺序执行。 \ No newline at end of file diff --git a/Source/Builder/zh-CN/Message.psd1 b/Source/Builder/zh-CN/Message.psd1 new file mode 100644 index 0000000..b97ab32 --- /dev/null +++ b/Source/Builder/zh-CN/Message.psd1 @@ -0,0 +1,38 @@ +# Localized 2/1/2018 2:06 PM (GMT) 410:2.92.0533 Message.psd1 +# Builder PBLocalizedData.zh-CN + +ConvertFrom-StringData @' + +# ---- [ Localized Data ] --------------------------------------------- + +Err_InvalidTaskName = 任务的名称不得为“null”或者空字符串。 +Err_TaskNameDoesNotExist = 任务不存在:{0} +Err_CircularReference = 任务存在循环引用:{0} +Err_MissingActionParameter = 使用“PreAction”或“PostAction”参数时必须同时指定“Action”参数:{0} +Err_CorruptCallStack = 调用堆栈已损坏(预值“{0}”,但实值为“{1}”)。 +Err_EnvPathDirNotFound = 指定的环境路径不存在,或者不是文件系统的文件夹:{0} +Err_BadCommand = 执行命令时发生了错误:{0} +Err_DefaultTaskCannotHaveAction = 任务“default”不得定义“Action”。 +Er_DuplicateTaskName = 任务已定义:{0} +Err_DuplicateAliasName = 别名已定义:{0} +Err_InvalidIncludePath = 未找到文件,无法插入文件内容:{0} +Err_BuildFileNotFound = 无法找到搭建文件:{0} +Err_NoDefaultTask = 需要一个名为“default”的任务。 +Err_LoadingModule = 加载模块时发生了错误:{0} +Err_LoadConfig = 加载搭建设置时发生了错误:{0} +RequiredVarNotSet = 执行任务“{1}”前必须设定变量“{0}”。 +PostconditionFailed = 任务的事后条件判断为失败:{0} +PreconditionWasFalse = 前提条件为“false”,任务将不被执行:{0} +ContinueOnError = 执行任务“{0}”时发生了错误:{1} +BuildSuccess = 搭建成功! +RetryMessage = 第{0}/{1}次尝试失败,在{2}秒后再次尝试... +BuildTimeReportTitle = 搭建用时报告 +Divider = ---------------------------------------------------------------------- +ErrorHeaderText = 发生了一个错误。详情如下: +ErrorLabel = 错误 +VariableLabel = 脚本变量: +DefaultTaskNameFormat = 执行{0} +UnknownError = 发生了一个未知的错误。 + +# ---- [ /Localized Data ] -------------------------------------------- +'@ diff --git a/THIRD-PARTY-LICENSE.txt b/THIRD-PARTY-LICENSE.txt new file mode 100644 index 0000000..2f506fb --- /dev/null +++ b/THIRD-PARTY-LICENSE.txt @@ -0,0 +1,34 @@ +THIRD PARTY LICENSES +==================== +Builder uses third-party libraries or other resources that may be distributed under licenses different than the Builder software. + +In the event that we accidentally failed to list a required notice, please bring it to our attention. The easiest way to do that is by creating a new issue labelled 'addlicense' in the 'Issues' section of our project page. You can also send us an email with the subject 'Third party license request' at: + + thirdpartylicense@lizoc.com + +The attached notices are provided for information only (last updated on 24 May, 2017). + + + +License notice for Psake +--------------------------------- +psake +Copyright (c) 2012-13 James Kovacs, Damian Hickey and Contributors + +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. diff --git a/Tools/PSMaml.psm1 b/Tools/PSMaml.psm1 new file mode 100644 index 0000000..ac9a232 --- /dev/null +++ b/Tools/PSMaml.psm1 @@ -0,0 +1,788 @@ +function ConvertFrom-Maml +{ + [CmdletBinding(DefaultParameterSetName = 'ByString')] + Param( + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'ByString')] + [string]$MAML, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] + [string]$Path + ) + + if ($PSCmdlet.ParameterSetName -eq 'ByPath') + { + $MAML = (Get-Content -Path $Path) -join [Environment]::NewLine + } + + $xmlObj = [xml]$MAML + + $outResult = @{} + + $xmlObj.helpItems.command | ForEach-Object { + $commandName = $_.details.name + + $synopsis = @($_.details.description.para | ForEach-Object { $_ }) + + $description = @($_.description.para | ForEach-Object { $_ }) + + $role = $_.role + $component = $_.component + $functionality = $_.functionality + + $commandParams = @{} + $_.parameters.parameter | ForEach-Object { + $commandParams."$($_.name)" = @($_.description.para | ForEach-Object { $_ }) + } + + $notes = @() + for ($i = 0; $i -lt $_.alertSet.alert.para.Count; $i++) + { + $notes += $_.alertSet.alert.para[$i] + } + + $links = @() + $_.relatedLinks.navigationLink | ForEach-Object { + $links += @{ + text = $_.linkText + url = $_.uri + } + } + + $examples = @{} + for ($i = 0; $i -lt $_.examples.example.Count; $i++) + { + $examples."example_$i" = @{} + + $egCode = $_.examples.example[$i].code + + $egRemark = @() + if (($_.examples.example[$i].remarks.para.Count -ge 1) -and + ($_.examples.example[$i].remarks.para[0] -ne '')) + { + if (($_.examples.example[$i].remarks.para[0] -eq '') -or + ($_.examples.example[$i].remarks.para[0] -eq $null)) + { + $_.examples.example[$i].remarks.para | where { $_ -ne '' } | ForEach-Object { + $egRemark += $_ + } + } + else + { + $remarkFirstPara = $_.examples.example[$i].remarks.para[0].Split([Environment]::NewLine) + + if (($remarkFirstPara.Count -ge 2) -and + ($remarkFirstPara[0] -eq 'DESCRIPTION') -and + ($remarkFirstPara[1] -match '^-----*$')) + { + if ($remarkFirstPara.Count -ne 2) + { + @(2..($remarkFirstPara.Count - 1)) | ForEach-Object { + $egRemark += $remarkFirstPara[$_] + } + } + } + else + { + $remarkFirstPara | ForEach-Object { + $egRemark += $_ + } + } + + if ($_.examples.example[$i].remarks.para.Count -gt 1) + { + for ($k = 1; $k -lt $_.examples.example[$i].remarks.para.Count; $k++) + { + if ($_.examples.example[$i].remarks.para[$k]) + { + $egRemark += $_.examples.example[$i].remarks.para[$k] + } + } + } + } + } + + $examples."example_$i" = @{ + code = $egCode + remarks = @($egRemark) + } + } + + $outText = @() + + $outText += '.SYNOPSIS' + $outText += ($synopsis | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine * 2) + $outText += '' + + if ($role) + { + $outText += '.ROLE' + $outText += ' ' + $role + $outText += '' + } + + if ($component) + { + $outText += '.COMPONENT' + $outText += ' ' + $component + $outText += '' + } + + if ($functionality) + { + $outText += '.FUNCTIONALITY' + $outText += ' ' + $functionality + $outText += '' + } + + $outText += '.DESCRIPTION' + $outText += ($description | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine * 2) + $outText += '' + $commandParams.Keys | sort | ForEach-Object { + if ($commandParams."$_") + { + $outText += '.PARAMETER ' + $_ + $outText += ($commandParams."$_" | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine * 2) + $outText += '' + } + } + + $examples.Keys | sort | ForEach-Object { + $outText += '.EXAMPLE' + if ($examples."$_".code) + { + $outText += ($examples."$_".code.Split([Environment]::NewLine) | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine) + } + if ($examples."$_".remarks) + { + $outText += '' + $outText += ' DESCRIPTION' + $outText += ' -----------' + $outText += ($examples."$_".remarks | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine * 2) + } + $outText += '' + $outText += '' + $outText += '' + } + + if ($notes.Count -gt 0) + { + $outText += '.NOTE' + $outText += ($notes | ForEach-Object { (' ' * 4) + $_ }) -join ([Environment]::NewLine * 2) + $outText += '' + } + + $links | ForEach-Object { + if ($_.text -or $_.url) + { + $outText += '.LINK' + } + if ($_.text) + { + $outText += ' ' + ($_.text) + } + if ($_.url) + { + $outText += ' ' + ($_.url) + } + if ($_.text -or $_.url) + { + $outText += '' + } + } + + $outResult."$commandName" = @{ + synopsis = $synopsis + description = $description + role = $role + component = $component + functionality = $functionality + parameters = $commandParams + notes = $notes + links = $links + examples = $examples + text = $outText -join [Environment]::NewLine + } + } + + $outResult +} + +function ConvertTo-Maml +{ + [CmdletBinding(DefaultParameterSetName = 'ByNameSet')] + Param( + [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'ByNameSet')] + [string[]]$Command, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByObjectSet')] + [pscustomobject[]]$HelpInfo, + + [Parameter()] + [switch]$Compress, + + [Parameter()] + [string]$OutFile + ) + + <# + https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents + + XML escape char table: + " " + ' ' + < < + > > + & & + + W3C spec: + Text escape ", ' and > is optional + Attrib escape > is optional. escape ' is optional if quote is in " and vice verse. + Comment do not escape any! + CDATA do not escape any! + Processing do not escape any! + + MSXML spec: + Text does not escape ' and " + Attrib quote is always in ". Does not escape ' + #> + + function EscXmlText + { + Param( + [Parameter(Mandatory, Position = 1)] + [string]$Text + ) + + # replace & first! + $Text.Replace( + '&', '&' + ).Replace( + '<', '<' + ).Replace( + '>', '>' + ) + } + + function EscXmlAttrib + { + Param( + [Parameter(Mandatory, Position = 1)] + [string]$Text + ) + + $Text.Replace( + '&', '&' + ).Replace( + '<', '<' + ).Replace( + '>', '>' + ).Replace( + '"', '"' + ) + } + + <# + Reference + --------- + https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-5.1&viewFallbackFrom=powershell-Microsoft.PowerShell.Core + https://msdn.microsoft.com/en-us/library/bb525433(v=vs.85).aspx + https://info.sapien.com/index.php/scripting/scripting-help/parameter-attributes-in-powershell-help + + TODO #notimplemented + - parameter variablelength + - parameter alias + - FORWARDHELPTARGETNAME + - FORWARDHELPCATEGORY + - REMOTEHELPRUNSPACE + #> + + $outMaml = @() + + $outMaml += '' + $outMaml += '' + + if ($PSCmdlet.ParameterSetName -eq 'ByNameSet') + { + $HelpInfo = @() + + foreach ($commandItem in $Command) + { + $HelpInfo += Get-Help $commandItem -Full + } + } + + foreach ($helpResult in $HelpInfo) + { + if (-not $helpResult) + { + continue + } + + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $helpResult.Name) + $outMaml += ' ' + if ($helpResult.details.description) + { + $helpResult.details.description | ForEach-Object { + if ($_.Text) + { + $outMaml += ' {0}' -f (EscXmlText $_.Text) + } + } + } + else + { + $outMaml += ' ' + } + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' {0}' -f $(if ($helpResult.Name -like '*-*') { EscXmlText ($helpResult.Name.Split('-')[0]) } else { '' }) + $outMaml += ' {0}' -f $(if ($helpResult.Name -like '*-*') { EscXmlText ($helpResult.Name.Substring($helpResult.Name.IndexOf('-') + 1)) } else { '' }) + $outMaml += ' ' + $outMaml += ' ' + + $outMaml += ' ' + $helpResult.description | where { $_ -ne $null } | ForEach-Object { + if ($_.Text) + { + $_.Text.Split([Environment]::NewLine) | where { $_ -ne '' } | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $_) + } + } + } + + $outMaml += ' ' + + $outMaml += ' ' + $helpResult.syntax.syntaxItem | ForEach-Object { + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $_.name) + $_.parameter | where { $_ -ne $null } | ForEach-Object { + $paramRequired = $(if ($_.required) { $_.required } else { 'false' }) + $paramGlobbing = $(if ($_.globbing) { $_.globbing } else { 'false' }) + + $outMaml += ' ' -f (EscXmlAttrib $paramRequired), (EscXmlAttrib $paramGlobbing), (EscXmlAttrib $_.pipelineInput), (EscXmlAttrib $_.position) + $outMaml += ' {0}' -f (EscXmlText $_.Name) + if ($_.parameterValue) + { + $outMaml += ' {1}' -f (EscXmlAttrib $paramRequired), (EscXmlText $_.parameterValue) + } + $outMaml += ' ' + } + $outMaml += ' ' + } + $outMaml += ' ' + + $outMaml += ' ' + $helpResult.parameters.parameter | where { $_ -ne $null } | ForEach-Object { + $paramRequired = $(if ($_.required) { $_.required } else { 'false' }) + $paramGlobbing = $(if ($_.globbing) { $_.globbing } else { 'false' }) + + $outMaml += ' ' -f (EscXmlAttrib $paramRequired), (EscXmlAttrib $paramGlobbing), (EscXmlAttrib $_.pipelineInput), (EscXmlAttrib $_.position) + $outMaml += ' {0}' -f (EscXmlText $_.name) + $outMaml += ' ' + if ($_.description.Text) + { + $_.description.Text.Split([Environment]::NewLine) | where { $_ -ne '' } | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $_) + } + } + $outMaml += ' ' + $outMaml += ' {1}' -f (EscXmlAttrib $_.required), (EscXmlText $_.type.name) + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $_.type.name) + $outMaml += ' ' + $outMaml += ' ' + if ($_.defaultValue) + { + $outMaml += ' {0}' -f (EscXmlText $_.defaultValue) + } + <# + $outMaml += '' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += '/' + #> + $outMaml += ' ' + } + $outMaml += ' ' + + $outMaml += ' ' + if ($helpResult.inputTypes.inputType.type.name) + { + # String[], PSObject[] + # http://somewhere/optional + # + # This is a description. + # + # Another line. + # + # -------------------- + # + # Int[] + # Some other lines. + + $inputTypeRawText = $helpResult.inputTypes.inputType.type.name.Split([Environment]::NewLine) | where { $_ -ne '' } + $inputTypeSection = @() + for ($i = 0; $i -lt $inputTypeRawText.Count; $i++) + { + if ($inputTypeRawText[$i] -notmatch '^-----*$') + { + $inputTypeSection += $inputTypeRawText[$i] + + if ($i -ne ($inputTypeRawText.Count - 1)) + { + continue + } + } + + if ($inputTypeSection.Count -eq 0) + { + continue + } + + $typeName = $inputTypeSection[0] + $typeUrl = $null + $typeDesc = @() + + if ($inputTypeSection.Count -gt 1) + { + if (($inputTypeSection[1] -like 'http://*') -or + ($inputTypeSection[1] -like 'https://*')) + { + $typeUrl = $inputTypeSection[1] + } + else + { + $typeDesc += $inputTypeSection[1] + } + } + + if ($inputTypeSection.Count -gt 2) + { + @(2..($inputTypeSection.Count - 1)) | ForEach-Object { + $typeDesc += $inputTypeSection[$_] + } + } + + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $typeName) + if ($typeUrl -eq $null) + { + $outMaml += ' ' + } + else + { + $outMaml += ' {0}' -f (EscXmlText $typeUrl) + } + $outMaml += ' ' + $outMaml += ' ' + $typeDesc | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $_) + } + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + + # reset section content holder + $inputTypeSection = @() + } + } + else + { + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' None' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + } + $outMaml += ' ' + + $outMaml += ' ' + if ($helpResult.returnValues.returnValue.type.name) + { + # String[], PSObject[] + # http://somewhere/optional + # + # This is a description. + # + # Another line. + # + # -------------------- + # + # Int[] + # Some other lines. + + $returnTypeRawText = $helpResult.returnValues.returnValue.type.name.Split([Environment]::NewLine) | where { $_ -ne '' } + $returnTypeSection = @() + for ($i = 0; $i -lt $returnTypeRawText.Count; $i++) + { + if ($returnTypeRawText[$i] -notmatch '^-----*$') + { + $returnTypeSection += $returnTypeRawText[$i] + + if ($i -ne ($returnTypeRawText.Count - 1)) + { + continue + } + } + + if ($returnTypeSection.Count -eq 0) + { + continue + } + + $typeName = $returnTypeSection[0] + $typeUrl = $null + $typeDesc = @() + + if ($returnTypeSection.Count -gt 1) + { + if (($returnTypeSection[1] -like 'http://*') -or + ($returnTypeSection[1] -like 'https://*')) + { + $typeUrl = $returnTypeSection[1] + } + else + { + $typeDesc += $returnTypeSection[1] + } + } + + if ($returnTypeSection.Count -gt 2) + { + @(2..($returnTypeSection.Count - 1)) | ForEach-Object { + $typeDesc += $returnTypeSection[$_] + } + } + + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $typeName) + if ($typeUrl -eq $null) + { + $outMaml += ' ' + } + else + { + $outMaml += ' {0}' -f (EscXmlText $typeUrl) + } + $outMaml += ' ' + $outMaml += ' ' + $typeDesc | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $_) + } + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + + # reset section content holder + $returnTypeSection = @() + } + } + else + { + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' None' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + } + $outMaml += ' ' + + <# + $outMaml += ' ' + $outMaml += ' ' + #> + + if ($helpResult.Role) + { + $outMaml += ' {0}' -f (EscXmlText $helpResult.Role) + } + + if ($helpResult.Component) + { + $outMaml += ' {0}' -f (EscXmlText $helpResult.Component) + } + + if ($helpResult.Functionality) + { + $outMaml += ' {0}' -f (EscXmlText $helpResult.Functionality) + } + + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $helpResult.alertSet.alert | where { $_ -ne $null } | ForEach-Object { + if ($_.Text) + { + $_.Text.Split([Environment]::NewLine) | where { $_ -ne '' } | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $_) + } + } + } + $outMaml += ' ' + $outMaml += ' ' + + $outMaml += ' ' + $helpResult.examples.example | where { $_ -ne $null } | ForEach-Object { + $outMaml += ' ' + $outMaml += ' {0}' -f (EscXmlText $_.title) + $outMaml += ' ' + $_.introduction | ForEach-Object { + if ($_.Text) + { + $_.Text.Split([Environment]::NewLine) | where { $_ -ne '' } | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $(if ($_.EndsWith(' ')) { $_ } else { $_ + ' ' })) + } + } + } + $outMaml += ' ' + + $codeRemarkBlock = @() + if ($_.code) + { + $codeRemarkBlock += $_.code + } + $_.remarks | ForEach-Object { + if ($_.Text) + { + $_.Text.Split([Environment]::NewLine) | where { $_ -ne '' } | ForEach-Object { + $codeRemarkBlock += $_ + } + } + } + + # Split by DESCRIPTION followed by ----- + $codeBlock = @() + $remarkBlock = @() + for ($i = 0; $i -lt $codeRemarkBlock.Count; $i++) + { + if (($codeRemarkBlock[$i] -eq 'DESCRIPTION') -and + ($i -le ($codeRemarkBlock.Count - 2)) -and + ($codeRemarkBlock[$i + 1] -match '^-----*$')) + { + if ($i -eq ($codeRemarkBlock.Count - 2)) + { + break + } + + @(($i + 2)..($codeRemarkBlock.Count - 1)) | ForEach-Object { + $remarkBlock += $codeRemarkBlock[$_] + } + break + } + + $codeBlock += $codeRemarkBlock[$i] + } + + $outMaml += ' {0}' -f (EscXmlText ($codeBlock -join [Environment]::NewLine)) + $outMaml += ' ' + if ($remarkBlock.Count -gt 0) + { + # Need to put this in 1 para, otherwise there's a gap between them when rendered by ps :( + $outMaml += ' DESCRIPTION{0}-----------{0}{1}' -f [Environment]::NewLine, $remarkBlock[0] + } + + if ($remarkBlock.Count -gt 1) + { + @(1..($remarkBlock.Count - 1)) | ForEach-Object { + $outMaml += ' {0}' -f (EscXmlText $remarkBlock[$_]) + } + } + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + $outMaml += ' ' + } + $outMaml += ' ' + + $outMaml += ' ' + $helpResult.relatedLinks.navigationLink | where { $_ -ne $null } | ForEach-Object { + if ($_.linkText -or $_.uri) + { + $outMaml += ' ' + $outMaml += $( + if ($_.linkText) + { + ' {0}' -f (EscXmlText $_.linkText) + } + else + { + ' ' + } + ) + $outMaml += $( + if ($_.uri) + { + ' {0}' -f (EscXmlText $_.uri) + } + else + { + ' ' + } + ) + $outMaml += ' ' + } + } + $outMaml += ' ' + + $outMaml += ' ' + } + + $outMaml += '' + + if (-not $Compress) + { + if ($OutFile) + { + $outMaml | Set-Content -Path $OutFile -Encoding UTF8 + } + else + { + $outMaml + } + } + else + { + $outXml = [xml]$outMaml + if ($OutFile) + { + $outXml.OuterXml | Set-Content -Path $OutFile -Encoding UTF8 + } + else + { + $outXml.OuterXml + } + } +} + +# ----------- +# Export +# ----------- +Export-ModuleMember -Function @( + 'ConvertFrom-Maml', 'ConvertTo-Maml' +) diff --git a/Tools/PSTemplate.psm1 b/Tools/PSTemplate.psm1 new file mode 100644 index 0000000..8d41a94 --- /dev/null +++ b/Tools/PSTemplate.psm1 @@ -0,0 +1,390 @@ +#Requires -Version 2.0 + +if ($PSVersionTable.PSVersion.Major -ge 3) +{ + $script:IgnoreError = 'Ignore' +} +else +{ + $script:IgnoreError = 'SilentlyContinue' +} + + +####################################################################### +# Public Module Functions +####################################################################### + +function Expand-PSTemplate +{ + <# + .SYNOPSIS + Render a text template using PowerShell templating syntax. + + .DESCRIPTION + Surround your script with '{{' and '}}'; any text outside will be interpreted as literal text. + + You need to use the `DataBinding` parameter to define custom variables and functions. + + .PARAMETER Template + A text template written using PowerShell templating syntax. + + .PARAMETER DataBinding + A hashtable containing variables and custom functions. + + .EXAMPLE + Expand-PSTemplate @' + hello world! + '@ + + hello world! + + DESCRIPTION + ----------- + If the template does not contain any script, it is displayed as is. + + .EXAMPLE + @{ + foo = 'bar' + } | Expand-PSTemplate 'hello {{ $foo }}' + + hello bar + + DESCRIPTION + ----------- + Keys in the `DataBinding` hashtable can generally be used as ordinary variables within the template. + + You need to ensure that the value can be converted to a string type. + + .EXAMPLE + @{ + foo = 'bar' + concat = [scriptblock]{ + [cmdletbinding()] + param( + [parameter(mandatory = $true, valuefrompipeline = $true)] + [string]$inputObject, + + [parameter(position = 1)] + [string]$otherObject + ) + + return ('{0}{1}' -f $inputObject, $otherObject) + } + } | Expand-PSTemplate 'hello {{ $foo | concat 'guy' }}' + + hello barguy + + DESCRIPTION + ----------- + You can define custom functions by creating [scriptblock] entries in the `DataBinding` hashtable. + + .EXAMPLE + @{ + 'cars' = @('honda', 'ford', 'bmw') + 'fruits' = @{ + 'apple' = 'red' + 'banana' = 'yellow' + } + } | Expand-PSTemplate @' + Cars at index 0 is {{ $cars[0] }}. + Here are all the cars: + {{ $cars | % { }} + * {{ $_ }}{{ } }} + + The color of fruit apple is {{ $fruits.apple }} + Here are all the fruits and their colors: + {{ $fruits.Keys | % { }} + * {{ $_ }} = {{ $fruits."$_" }}{{ } }} + + DESCRIPTION + ----------- + Use the `ForEach-Object` (or `%` alias) to loop through collections. + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 1)] + [string]$Template, + + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [hashtable]$DataBinding + ) + + # separate scriptblocks from common vars + $templateVars = @{} + $templateFuncs = @() + + foreach ($dataKey in $DataBinding.Keys) + { + if ($DataBinding."$dataKey" -is [scriptblock]) + { + $scriptDef = @() + $scriptDef += 'function {0}' -f $dataKey + $scriptDef += $DataBinding."$dataKey".ToString() + $templateFuncs += ($scriptDef -join [Environment]::NewLine) + } + else + { + $templateVars."$dataKey" = $DataBinding."$dataKey" + } + } + + # shorthand helper + $enc = [System.Text.Encoding]::UTF8 + + # this will be invoke-expression-ed + $evalScript = [System.Text.StringBuilder]::new() + + $evalHeader = @( + 'Param(' + ' [Parameter(Mandatory, Position = 1)]' + ' [hashtable]$Data' + ')' + '' + 'foreach ($dataKey in $Data.Keys)' + '{' + ' if (Test-Path "Variable:\$dataKey")' + ' {' + ' Set-Item -Path "Variable:\$dataKey" -Value $Data.$dataKey -WhatIf:$false -Confirm:$false | Out-Null' + ' }' + ' else' + ' {' + ' New-Item -Path "Variable:\$dataKey" -Value $Data.$dataKey -WhatIf:$false -Confirm:$false | Out-Null' + ' }' + '}' + ) -join [Environment]::NewLine + + $evalScript.AppendLine($evalHeader) | Out-Null + + # hard-code the delims to mustache + $delimStart = '{{' + $delimEnd = '}}' + + $ps = Select-Substring -InputObject $Template -Preceding $delimStart -Succeeding $delimEnd -PassThru + + # force to array + if (($ps -ne $null) -and ($ps.Count -eq $null)) + { + $ps = @($ps) + } + + # template does not require processing + if ($ps.Count -eq 0) + { + return $Template + } + + # generate the main script + for ($i = 0; $i -lt $ps.Count; $i++) + { + if ($i -eq 0) + { + $payload = $Template.Substring(0, $ps[0].Preceding.Index) + } + else + { + $payload = $Template.Substring($ps[$i - 1].Succeeding.Index + $delimEnd.Length, $ps[$i].Preceding.Index - $ps[$i - 1].Succeeding.Index - $delimEnd.Length) + } + + # add preceding payload + if ($payload) + { + $payloadEnc = [Convert]::ToBase64String($enc.GetBytes($payload), [Base64FormattingOptions]::None) + $evalScript.AppendLine('[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("{0}"))' -f $payloadEnc) | Out-Null + } + + # add ps command + if ($ps[$i].Substring) + { + $evalScript.AppendLine($ps[$i].Substring) | Out-Null + } + + # add last payload + if ($i -eq ($ps.Count - 1)) + { + $payload = $Template.Substring($ps[$i].Succeeding.Index + $delimEnd.Length) + if ($payload) + { + $payloadEnc = [Convert]::ToBase64String($enc.GetBytes($payload), [Base64FormattingOptions]::None) + $evalScript.AppendLine('[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("{0}"))' -f $payloadEnc) | Out-Null + } + } + } + + # create ps runspace + $initialSessionState = [initialsessionstate]::CreateDefault2() + $runspace = [runspacefactory]::CreateRunspace($initialSessionState) + $psInstance = [powershell]::Create() + $psInstance.Runspace = $runspace + $runspace.Open() + + # add the custom funcs + if ($templateFuncs.Count -gt 0) + { + $templateFuncs | ForEach-Object { + Write-Verbose "Add function: $_" + [void]$psInstance.AddStatement().AddScript($_) + } + } + + # add the main script + Write-Verbose ("Main: " -f $evalScript.ToString()) + [void]$psInstance.AddStatement().AddScript($evalScript.ToString()).AddArgument($templateVars) + + # generate + $result = $psInstance.Invoke() + $psInstance.Dispose() + $runspace.Dispose() + + # return result + $result -join '' +} + +function Select-Substring +{ + <# + .SYNOPSIS + Search for a substring that has the specified text appearing before and after it. + + .DESCRIPTION + Using regex to seaech for indeterminate length patterns can have a big performance hit when text length increases. This command uses loops and substring indexes internally to allow faster searches for long strings. + + To obtain the position of each preceding, succeeding, and substring positions, use the 'PassThru' switch. + + .Example + Select-Substring -InputObject "a !banana, and an !apple," -Before 'a', '!' -After ',' + #banana + #apple + #> + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] + [string]$InputObject, + + [Parameter(Mandatory = $true, Position = 2)] + [Alias('Before')] + [string[]]$Preceding, + + [Parameter(Mandatory = $true, Position = 3)] + [Alias('After')] + [string[]]$Succeeding, + + [Parameter(Mandatory = $false)] + [Switch]$CaseSensitive, + + [Parameter(Mandatory = $false)] + [ValidateSet('FixedExactlyOnce')] + [string]$OrderBy = 'FixedExactlyOnce', + + [Parameter(Mandatory = $false)] + [switch]$PassThru + ) + + Begin + { + function GetIndexOfTags([Int]$startIndex) + { + if (-not $CaseSensitive) + { + $searchData = $InputObject.ToLower() + } + else + { + $searchData = $InputObject + } + + $lastCursor = $startIndex + + $startTags = @() + foreach ($stag in $Preceding) + { + if (-not $CaseSensitive) + { + $stag = $stag.ToLower() + } + $thisCursor = $searchData.IndexOf($stag, $lastCursor) + + if ($thisCursor -eq -1) + { + return $null + } + else + { + $startTags += [PSCustomObject]@{ + 'Value' = $InputObject.Substring($thisCursor, $stag.Length) + 'Index' = $thisCursor + 'Length' = $stag.Length + } + + $lastCursor = $thisCursor + $stag.Length + } + } + + $endTags = @() + foreach ($stag in $Succeeding) + { + if (-not $CaseSensitive) + { + $stag = $stag.ToLower() + } + $thisCursor = $searchData.IndexOf($stag, $lastCursor) + + if ($thisCursor -eq -1) + { + return $null + } + else + { + $endTags += [PSCustomObject]@{ + 'Value' = $InputObject.Substring($thisCursor, $stag.Length) + 'Index' = $thisCursor + 'Length' = $stag.Length + } + + $lastCursor = $thisCursor + $stag.Length + } + } + + return [PSCustomObject]@{ + 'Preceding' = $startTags + 'Succeeding' = $endTags + 'Substring' = '' + } + } + } + + Process + { + $startCursor = 0 + $output = @() + while ($true) + { + $result = GetIndexOfTags($startCursor) + if ($result -eq $null) { break } + + $result.Substring = $InputObject.Substring( + $result.Preceding[-1].Index + $result.Preceding[-1].Length, + $result.Succeeding[0].Index - $result.Preceding[-1].Index - $result.Preceding[-1].Length) + + $output += $result + $startCursor = $result.Succeeding[-1].Index + $result.Succeeding[-1].Length + } + } + + End + { + if ($PassThru) + { + $output + } + else + { + $output | ForEach-Object { $_.Substring } + } + } +} + +Export-ModuleMember -Function @( + 'Expand-PSTemplate' +) diff --git a/Tools/Robocopy.psm1 b/Tools/Robocopy.psm1 new file mode 100644 index 0000000..ca864e7 --- /dev/null +++ b/Tools/Robocopy.psm1 @@ -0,0 +1,151 @@ +function Invoke-Robocopy +{ + [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + Param( + [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] + [string]$SourcePath, + + [Parameter(Mandatory = $true, Position = 2)] + [string]$DestinationPath, + + [Parameter(Mandatory = $true, ParameterSetName = 'MirrorSet')] + [switch]$Mirror, + + [Parameter(Mandatory = $false)] + [string[]]$ExcludeFolder, + + [Parameter(Mandatory = $false)] + [string[]]$ExcludeFile, + + [Parameter(Mandatory = $false)] + [switch]$Silent + ) + + # np - no progress + # ns - don't file size + # nfl - don't show file name + # ndl - don't show dir name + # njs - no job summary + # njh - no job header + + # caveats: the current parsing implementation does not work with /njs + $cpOptions = @('/NP', '/NS', '/NC', '/NFL', '/NDL', '/NJS') + + # mir - mirror + if ($PSCmdlet.ParameterSetName -eq 'MirrorSet') + { + $cpOptions += '/MIR' + } + + # /xd dirs [dirs] - exclude dirs matching given name/path + if ($ExcludeFolder) + { + $cpOptions += '/XD ' + (($ExcludeFolder | ForEach-Object { '"{0}"' -f $_ }) -join ' ') + } + + # /xf file [file] - exclude files matching given name/paths/wildcard + if ($ExcludeFile) + { + $cpOptions += '/XF ' + (($ExcludeFile | ForEach-Object { '"{0}"' -f $_ }) -join ' ') + } + + # run it! + # https://stackoverflow.com/questions/18923315/using-in-powershell + $sourceLiteralPath = Resolve-Path $SourcePath | select -expand Path + $destLiteralPath = Resolve-Path $DestinationPath | select -expand Path + + # strangely, if you want to quote your paths, robocopy requires "double quote", not 'single quote' + $copyArgs = ('"{0}" "{1}" ' -f $sourceLiteralPath, $destLiteralPath) + ($cpOptions -join ' ') + Write-Verbose "Executing external program: robocopy $copyArgs" + $copyResult = robocopy '--%' $copyArgs + $copyResult = $copyResult | Out-String + Write-Verbose "Execution result: $copyResult" + + # Silent -- just return as is + if ($Silent) { return } + + + # Attempt to parse output! + + $copyResultObj = @{} + $parseNextLine = $false + $parseNextField = '' + $parseBreakPattern = '' + + $copyResultList = $copyResult.Split([Environment]::NewLine) + for ($i = 0; $i -lt $copyResultList.Count; $i++) + { + if ($copyResultList[$i] -eq $null) { continue } + if ($copyResultList[$i] -eq '') { continue } + + if ($parseNextLine -eq $true) + { + if ($copyResultList[$i] -clike $parseBreakPattern) + { + $parseNextLine = $false + $parseNextField = '' + $parseBreakPattern = '' + } + else + { + $copyResultObj."$parseNextField" += $copyResultList[$i].Trim() + continue + } + } + + if ($copyResultList[$i] -like '*----*') { continue } + if ($copyResultList[$i] -clike '*ROBOCOPY*') { continue } + + if ($copyResultList[$i].Trim() -clike 'Started : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Started : ') + 'Started : '.Length) + $copyResultObj.StartTime = [datetime]::Parse($parseValue) + } + elseif ($copyResultList[$i].Trim() -clike 'Source : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Source : ') + 'Source : '.Length) + $copyResultObj.Source = $parseValue + } + elseif ($copyResultList[$i].Trim() -clike 'Dest : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Dest : ') + 'Dest : '.Length) + $copyResultObj.Destination = $parseValue + } + elseif ($copyResultList[$i].Trim() -clike 'Files : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Files : ') + 'Files : '.Length) + $copyResultObj.Files = $parseValue + } + elseif ($copyResultList[$i].Trim() -clike 'Options : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Options : ') + 'Options : '.Length) + $copyResultObj.Options = $parseValue + } + elseif ($copyResultList[$i].Trim() -clike 'Exc Files : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Exc Files : ') + 'Exc Files : '.Length) + $copyResultObj.ExcludeFiles = @($parseValue) + $parseNextLine = $true + $parseNextField = 'ExcludeFiles' + $parseBreakPattern = '* : *' + } + elseif ($copyResultList[$i].Trim() -clike 'Exc Dirs : *') + { + $parseValue = $copyResultList[$i].Substring($copyResultList[$i].IndexOf('Exc Dirs : ') + 'Exc Dirs : '.Length) + $copyResultObj.ExcludeFolders = @($parseValue) + $parseNextLine = $true + $parseNextField = 'ExcludeFolders' + $parseBreakPattern = '* : *' + } + } + + [pscustomobject]$copyResultObj +} + + +# ----------- +# Export +# ----------- +Export-ModuleMember -Function @( + 'Invoke-Robocopy' +) diff --git a/Tools/build.ps1 b/Tools/build.ps1 new file mode 100644 index 0000000..60f4502 --- /dev/null +++ b/Tools/build.ps1 @@ -0,0 +1,272 @@ +$toolsDir = $PSScriptRoot +$repoDir = Resolve-Path (Join-Path $toolsDir -ChildPath '..') | select -expand Path +$sourceDir = Join-Path $repoDir -ChildPath 'Source' +$workingDir = Join-Path $repoDir -ChildPath 'Working' +$repoName = Split-Path $repoDir -Leaf +$releaseDir = Join-Path $repoDir -ChildPath 'Releases' +$credDir = Join-Path $repoDir -ChildPath 'Credentials' + +dir (Join-Path $toolsDir -ChildPath '*.psm1') | ForEach-Object { + Write-Output ("Importing add-on module: Tools/{0}" -f $_.Name) + ipmo $_.FullName -Force +} + +# create working dir +@($releaseDir, $workingDir) | ForEach-Object { + if (-not (Test-Path $_)) + { + Write-Output ('Creating directory: {0}' -f $_.Substring($repoDir.Length)) + md $_ -Force | Out-Null + } + else + { + if (Test-Path $_ -PathType Leaf) + { + throw ("You need to remove the file '{0}'" -f $_) + } + } +} + +# copy all from source to working folder +Write-Output 'Mirror source to working folder' +Invoke-Robocopy -SourcePath $sourceDir -DestinationPath $workingDir -Mirror -Verbose + +Write-Output 'Preparing templating data' +$tmplData = ConvertFrom-Json (Get-Content -Path (Join-Path $toolsDir -ChildPath 'projectInfo.json') -Raw) +$tmplDataHash = @{} +$tmplData | Get-Member -MemberType NoteProperty | select -expand Name | ForEach-Object { + $tmplDataHash."$_" = $tmplData."$_" +} +# dynamic vars +$tmplDataHash.repoDir = $repoDir + +Write-Output 'Importing template language helper functions' +[Management.Automation.Language.Parser]::ParseInput((Get-Content -Path (Join-Path $toolsDir -ChildPath 'template_helpers.ps1') -Raw), [ref]$null, [ref]$null).FindAll({ + param($ast) + $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] +}, $false) | ForEach-Object { + $tmplDataHash."$($_.Name)" = [scriptblock]::Create($_.Body.Extent.Text) +} + +Write-Output 'Generating from templates' +dir (Join-Path $workingDir -ChildPath '*.pstmpl') -Recurse -File | ForEach-Object { + $tmplFilePath = $_.FullName + $tmplFileName = $_.Name + $tmplFileDir = $_.Directory.FullName + $tmplText = Get-Content $tmplFilePath -Raw + + $outputName = $tmplFileName.Substring(0, $tmplFileName.Length - '.pstmpl'.Length) + $outputPath = Join-Path $tmplFileDir -ChildPath $outputName + + $tmplDataHash.tmplFile = $tmplFilePath + $tmplDataHash.pwd = $tmplFileDir + + Write-Output ('* {0} -> {1}' -f $tmplFilePath.Substring($workingDir.Length), $outputPath.Substring($workingDir.Length)) + $outContent = $tmplDataHash | Expand-PSTemplate -Template $tmplText + $outContent | Set-Content -Path $outputPath -Encoding UTF8 +} + +Write-Output 'Removing templates' +dir (Join-Path $workingDir -ChildPath '*.pstmpl') -Recurse -File | ForEach-Object { + Write-Output ('* {0}' -f $_.FullName.Substring($workingDir.Length)) + del $_ +} + +# --------------------------- + +$allProjects = dir $workingDir -Directory | where { $_.Name -notlike '*.Tests' } | select -expand FullName + +# en-US and en are special +$allCultures = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | where { + ($_ -ne '') -and + ($_ -ne $null) -and + ($_ -ne 'en-US') -and + ($_ -ne 'en') +} + +foreach ($projectDir in $allProjects) +{ + $projectName = Split-Path $projectDir -Leaf + + Write-Output ('Building project: {0}' -f $projectName) + + @("$projectName.psd1", "$projectName.psm1") | ForEach-Object { + if (-not (Test-Path (Join-Path $workingDir -ChildPath "$projectName/$_") -PathType Leaf)) + { + Write-Warning ("The project cannot be built by this script because is not a PowerShell module: {0}" -f $projectName) + continue + } + } + + # regen en-US\{projectName}-Help.xml based on cbh + Write-Output ('* Creating default MAML help file') + ipmo (Join-Path $workingDir -ChildPath $projectName) -DisableNameChecking -Force + $exportCmd = Get-Command -Module $projectName | select -expand Name + ConvertTo-Maml -Command $exportCmd -Compress -OutFile (Join-Path $workingDir -ChildPath "$projectName/en-US/$projectName-Help.xml") + # use en-US as en + copy (Join-Path $workingDir -ChildPath "$projectName/en-US") (Join-Path $workingDir -ChildPath "$projectName/en") -Recurse + + Write-Output ('* Analyzing syntax tree: {0}' -f "$projectName.psm1") + $scriptText = Get-Content -Path (Join-Path $workingDir -ChildPath "$projectName/$projectName.psm1") -Raw + $scriptTokens = $null + $scriptErrors = $null + $scriptAst = [Management.Automation.Language.Parser]::ParseInput($scriptText, [ref]$scriptTokens, [ref]$scriptErrors) + $cbhTokens = $scriptTokens | where { + ($_.Kind -eq 'Comment') -and + ($_.Extent.Text -match '([\t\s]*)(\.SYNOPSIS)([\t\s]*)([\r\n]+)') + } + + $allParams = $scriptAst.FindAll({ + param($ast) + $ast -is [System.Management.Automation.Language.ParamBlockAst] + }, $true) + + $allCommands = @{} + + $allParams | ForEach-Object { + $paramDef = $_ + $commandName = $paramDef.Parent.Parent.Name + $codeStub = @() + $codeStub += $( + if ($paramDef.Parent.Parent.IsFilter) { 'filter {0}' -f $commandName } + elseif ($paramDef.Parent.Parent.IsWorkflow) { 'workflow {0}' -f $commandName } + elseif ($paramDef.Parent.Parent.IsConfiguration) { 'configuration {0}' -f $commandName } + else { 'function {0}' -f $commandName } + ) + $codeStub += '{' + $codeStub += '<# .EXTERNALHELP help.xml #>' + $paramDef.Attributes | ForEach-Object { + $codeStub += $_.Extent.Text + } + $codeStub += $paramDef.Extent.Text + $codeStub += '}' + + $allCommands."$commandName" = $codeStub + } + + Write-Output ('* Modifying module file to use external help') + $newScript = [System.Text.StringBuilder]::new() + for ($i = 0; $i -lt $cbhTokens.Count; $i++) + { + Write-Output ('** Processing {0}/{1} comment-based help entries' -f ($i + 1), $cbhTokens.Count) + if ($i -eq 0) + { + $newScript.Append($scriptText.Substring(0, $cbhTokens[$i].Extent.StartOffset)) | Out-Null + } + + $newScript.Append("# .EXTERNALHELP $projectName-Help.xml") | Out-Null + + if ($i -lt ($cbhTokens.Count - 1)) + { + $newScript.Append($scriptText.Substring($cbhTokens[$i].Extent.EndOffset, $cbhTokens[$i + 1].Extent.StartOffset - $cbhTokens[$i].Extent.EndOffset)) | Out-Null + } + else + { + $newScript.Append($scriptText.Substring($cbhTokens[$i].Extent.EndOffset)) | Out-Null + } + } + + Write-Output ('** Writing to {0}.psm1' -f $projectName) + $newScript.ToString() | Set-Content -Path (Join-Path $workingDir -ChildPath "$projectName/$projectName.psm1") + + # regen other cultures + foreach ($cultureCode in $allCultures) + { + $localizeDir = Join-Path $workingDir -ChildPath "$projectName/$cultureCode" + if (-not (Test-Path $localizeDir -PathType Container)) + { + continue + } + + Write-Output ('* Processing globalization resource: {0}' -f $cultureCode) + + $outMamlPath = Join-Path $localizeDir -ChildPath "$projectName-Help.xml" + $commandHelpTopics = dir (Join-Path $localizeDir -ChildPath "$projectName-Help_*.md") -File + if (-not $commandHelpTopics) + { + Write-Warning ("No file found that matches the wildcard pattern: {0}" -f "$projectName-Help_*.md") + continue + } + + $commandHelp = @{} + $commandHelpTopics | ForEach-Object { + $commandName = $_.BaseName.Substring("$repoName-Help_".Length) + $commandText = Get-Content -Path $_.FullName -Encoding UTF8 + $commandHelp."$commandName" = $commandText -join [Environment]::NewLine + } + + $localizeEvalStub = @() + + $commandHelp.Keys | ForEach-Object { + if ($_ -in $allCommands.Keys) + { + Write-Output ('** Generating comment-based help stub: {0}' -f $_) + + $addToStub = $allCommands."$_" + $addToStub[2] = '<#' + [Environment]::NewLine + $commandHelp."$_" + [Environment]::NewLine + '#>' + + $localizeEvalStub += $addToStub -join [Environment]::NewLine + } + } + + $commandHelp.Keys | ForEach-Object { + $localizeEvalStub += "Get-Help $_ -Full" + } + + Write-Output ('** Writing MAML file') + $evalScript = [scriptblock]::Create(($localizeEvalStub -join [Environment]::NewLine)) + $helpObjects= $evalScript.Invoke() + ConvertTo-Maml -HelpInfo $helpObjects -Compress -OutFile $outMamlPath + + Write-Output ('** Removing files: {0}' -f "$projectName-Help_*.md") + $commandHelpTopics | ForEach-Object { + del $_ -Force + } + } +} + +# -------------------------- + +$releaseVersion = $tmplDataHash.moduleVersion +if ($tmplDataHash.prerelease -eq 'True') +{ + $releaseVersionDir = Join-Path $releaseDir -ChildPath "v$releaseVersion-prerelease" +} +else +{ + $releaseVersionDir = Join-Path $releaseDir -ChildPath "v$releaseVersion" +} + +if (Test-Path $releaseVersionDir) +{ + rd $releaseVersionDir -Recurse -Force +} + +md $releaseVersionDir | Out-Null + +$commonPkgFiles = @( + 'LICENSE.txt', 'THIRD-PARTY-LICENSE.txt', 'README.md', 'icon.png' +) + +dir $workingDir -Directory | ForEach-Object { + Write-Output ('Generating package: {0}' -f $_.Name) + $pkgItemPaths = @() + $commonPkgFiles | ForEach-Object { + $pkgItemPaths += Join-Path $repoDir -ChildPath $_ + } + $pkgItemPaths += $_.FullName + + $psManifestPath = (Join-Path $workingDir -ChildPath ('{0}/{0}.psd1' -f $_.Name)) + if (Test-Path $psManifestPath -PathType Leaf) + { + $psManifest = Test-ModuleManifest -Path $psManifestPath + $pkgVersion = $psManifest.Version.ToString() + $pkgFileName = '{0}.{1}.zip' -f $_.Name, $pkgVersion + } + else + { + $pkgFileName = '{0}.zip' -f $_.Name + } + + Compress-Archive -Path $pkgItemPaths -DestinationPath (Join-Path $releaseVersionDir -ChildPath $pkgFileName) +} diff --git a/Tools/projectInfo.json b/Tools/projectInfo.json new file mode 100644 index 0000000..df7ddc0 --- /dev/null +++ b/Tools/projectInfo.json @@ -0,0 +1,42 @@ +{ + "moduleName": "Builder", + "moduleVersion": "3.1.1024.0", + "guid": "f611135c-e6d9-444d-a4a7-e8a15728942b", + "author": "Powershell Team", + "companyName": "Lizoc Inc.", + "copyright": "Copyright (c) 2018 Lizoc Inc. All rights reserved.", + "description": "Builder is a source code build system based on PowerShell.", + "synopsis": "Build system DSL based on PowerShell", + "category": "Scripting Techniques", + "docUrlBase": "http://buildcenter.github.io/Builder/", + "projectUrl": "http://www.github.com/buildcenter/Builder", + "prerelease": "False", + "requireLicenseAcceptance": "False", + "tags": [ "powershell", "buildtool" ], + "lastUpdate": "2018-2-4", + "licenseFamily": "MIT", + "downloadBaseUrl": "https://get.lizoc.com/", + + "requirePSVersion": "4.0", + "requireClrVersion": "3.5", + "platform": "None", + + "requirePSEdition": null, + "requireModule": null, + "requireAssembly": null, + "requireScript": null, + "requireHostVersion": null, + "requireHostName": null, + "requireDotNetVersion": null, + "moduleTypes": null, + "moduleFormats": null, + "nestedModule": null, + "exportFunction": null, + "exportCmdlet": null, + "exportVariable": null, + "exportAlias": null, + "moduleList": null, + "fileList": null, + "exportDsc": null, + "defaultCommandPrefix": null +} \ No newline at end of file diff --git a/Tools/publish.ps1 b/Tools/publish.ps1 new file mode 100644 index 0000000..c7492bd --- /dev/null +++ b/Tools/publish.ps1 @@ -0,0 +1,25 @@ +$toolsDir = $PSScriptRoot +$repoDir = Resolve-Path (Join-Path $toolsDir -ChildPath '..') | select -expand Path +$sourceDir = Join-Path $repoDir -ChildPath 'Source' +$workingDir = Join-Path $repoDir -ChildPath 'Working' +$repoName = Split-Path $repoDir -Leaf +$releaseDir = Join-Path $repoDir -ChildPath 'Releases' +$credDir = Join-Path $repoDir -ChildPath 'Credentials' + +$nugetApiKey = Get-Content (Join-Path $credDir -ChildPath 'powershell_gallery_nuget_apikey.txt') +#Import-LocalizedData -BindingVariable manifestData -BaseDirectory (Join-Path $workingDir -ChildPath 'Builder') -FileName 'Builder.psd1' + +# Possible issues: +# - PSModule name may already be taken in PSGallery +# - PowerShellGet requires NuGet.exe (PowerShellGet will prompt) +# - warns about Tags, ReleaseNotes, LicenseUri, ProjectUri (seems to be a bug with Publish-Module) +$publishModuleParams = @{ + Path = (Join-Path $workingDir -ChildPath 'Builder') + NuGetApiKey = $nugetApiKey + Repository = 'PSGallery' + #ReleaseNotes = $manifestData.PrivateData.PSData.ReleaseNotes + #Tags = $manifestData.PrivateData.PSData.Tags + #LicenseUri = $manifestData.PrivateData.PSData.LicenseUri + #ProjectUri = $manifestData.PrivateData.PSData.ProjectUri +} +Publish-Module @publishModuleParams diff --git a/Tools/template_helpers.ps1 b/Tools/template_helpers.ps1 new file mode 100644 index 0000000..38109ab --- /dev/null +++ b/Tools/template_helpers.ps1 @@ -0,0 +1,217 @@ +function concat +{ + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline = $true)] + [AllowEmptyString()] + [AllowNull()] + [string[]]$InputObject, + + [parameter(Position = 1)] + [AllowEmptyString()] + [string[]]$AppendWith = @('') + ) + + Begin + { + $appendText = $AppendWith -join '' + } + + Process + { + if ($InputObject -eq $null) + { + $InputObject = '' + } + + foreach ($inputItem in $InputObject) + { + if ($AppendWith) + { + '{0}{1}' -f $inputItem, $appendText + } + else + { + $inputItem + } + } + } +} + +function format +{ + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + [string[]]$InputObject, + + [parameter(Mandatory, Position = 1)] + [AllowEmptyString()] + [string]$With + ) + + Process + { + if ($InputObject -eq $null) + { + $InputObject = '' + } + + foreach ($inputItem in $InputObject) + { + if (-not $inputItem) + { + $With -f '' + } + else + { + $With -f $inputItem + } + } + } +} + +function include +{ + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline = $true)] + [string]$Path, + + [parameter()] + [string]$Indent, + + [parameter()] + [ValidateSet('Ascii', 'Unicode', 'UTF8')] + [string]$Encoding + ) + + if ((-not $Indent) -and (-not $Encoding)) + { + Get-Content -Path $Path -Raw + } + else + { + $getContentParam = @{ + Path = $Path + } + + if ($Encoding) + { + $getContentParam.Encoding = $Encoding + } + + (Get-Content @getContentParam | ForEach-Object { + if ($Indent) + { + $Indent + $_ + } + else + { + $_ + } + }) -join [Environment]::NewLine + } +} + +function lowercase +{ + [CmdletBinding()] + param( + [parameter(Mandatory, ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + [string[]]$InputObject + ) + + Process + { + if ($InputObject -eq $null) + { + $InputObject = '' + } + + foreach ($inputItem in $InputObject) + { + if ($inputItem -ne $null) + { + $inputItem.ToLowerInvariant() + } + else + { + $null + } + } + } +} + +function replace +{ + [CmdletBinding(DefaultParameterSetName = 'ReplaceEntireStringSet')] + param( + [parameter(Mandatory, ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + [string[]]$InputObject, + + [parameter(Mandatory, ParameterSetName = 'ReplaceNullSet')] + [parameter(ParameterSetName = 'ReplaceSubstringSet')] + [parameter(ParameterSetName = 'ReplaceEntireStringSet')] + [AllowEmptyString()] + [Alias('null')] + [string]$NullOrEmpty, + + [parameter(Mandatory, Position = 1, ParameterSetName = 'ReplaceSubstringSet')] + [string[]]$Substring, + + [parameter(Position = 2, ParameterSetName = 'ReplaceSubstringSet')] + [parameter(ParameterSetName = 'ReplaceEntireStringSet')] + [AllowEmptyString()] + [string]$With = '' + ) + + Process + { + if ($InputObject -eq $null) + { + $InputObject = '' + } + + foreach ($inputItem in $InputObject) + { + if ($PSBoundParameters.ContainsKey('NullOrEmpty') -and + (-not $inputItem)) + { + $NullOrEmpty + continue + } + + if ($PSCmdlet.ParameterSetName -eq 'ReplaceSubstringSet') + { + if (-not $inputItem) + { + '' + } + else + { + $replaceResult = $inputItem + foreach ($SubstringItem in $Substring) + { + $replaceResult = $replaceResult.Replace($SubstringItem, $With) + } + $replaceResult + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'ReplaceEntireStringSet') + { + $With + } + else + { + $inputItem + } + } + } +} diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..147f4530257eb95f84a7c3ccb77e4944d9558207 GIT binary patch literal 2743 zcmZ`*X*kqtAO4SFFqAFK3^A5)$}xs4$r@t|V`)%v#+bpWDI;WG#xi!HK}r!VG-RoS z$c)Jva>~)@a7cI^WQk;~^G?_K`o152&wbtZ^ZPx|{o%fz>&bL=u{#7sKmhz8*$My}^ThTDLVPY7Vt+mi0OXGSERch{ z`Wjz^hhx0MJt%?Uk%Z6yAcSx!I9w+cYANL=M+@LswgX1tz;&XmK?FvqUg*8qM2N3czKRW; z#CXLRUR+@Wy}qmHPbIyoG0{I!vlOpp&NLp|WNcgBG7ri$OcNl?4ra7kZ~CeT$7IZOOD@5mP=$zKK6%=a>k zg2JQi`hh1JMu{qd!DW0}0CIO}-32Ibqg25UVdOz)N#n!+l9Dg0&EEoZPgtBZ6=xE)aJufop#z#&`}PHSI1=Nn)@#`s%Pf31MePHAVV9Z6ELOQ}Jf^GZS!RWB zYUw}5Es%Pa7hsd3lR@Phfpks2ix4@~w6lg6|14Lth`UC&efVamv8zy#aP`(+^ z=nNhZ1X1)_Rl%?u`V~vKY0(CvTR1VXE!m<&(YUS(F0tqCpjLPFDXLBU z3o@>ckk%4gyGre!_5l$0iG2Zz(k5SrJdRu7E-OpIS-M%{JY@V(qE-P&oV|z;-dydN zaeiROH=yPJDzdS!)HbgVNb|$qfsW2-zUVk`BLUVSUiC%KI4DLRpz+5ip}7=_*2N8F z$E!qO@HX~_4#8>YX4~B1>iyM=lj<>}hLGx1tn1vSkxT{mdj!GB8Qm@e#hU(d{kh$d zrd&{XWErHUWdqADxzh>7^O`6f)8!BGB&ysT{#BP_Q-=R#NM!%BuCjz9@cWn|{IXon zi1yRMD7uQCr5NjTi|c+=X(*>YktzuR-^ZWE0%$;p#A=2E7(B#uxuv^zdU%-dh?AF0^c zw&128<7d)l+BTO>ETy4Oro>Ji7g_&S6Fb0@vO{8ElgAn zPUC|3{T)A=Eu)Nkb8au&pZGZ~e@&BJs9FBRGiALj{!7%c%5I>k0VM4)R$bIQCDqf- z($mc|MVN0x49po9Xdl9cvx&tSa^ONVMMq`&h)&r8Dod=N5(P{4xxpicrIz|kj4q|3 z2W+xI(7fO;&5!U&GtY`l-F#YTio3$LuXkFL&w%c>$%)PsXUc(RgIn)S6(V(m zHw&=kHKHDe@6<_m6jeH1?8Rh*#_ANhMGHCf5GE^EZC5{5+IyPpSrQiIJOTZKNPVZX zb~uqmajwrQIT?yGZPDOA$~^f%#tZV6!L=I8Y*0g$Z(NKp)8O^nq4v!z?*#iOT1 zFk2&e>i)pP7eVFEnt>Gro1t<^O2@lZqE;#a%WxP3jCJbc_wfg^q>E86oZrM@FzSJT zp{ZEnM#Fq*!1Qd189MN)wWxX!RZ>x|xNYls>#8<%ULzFoaW7rf4UGJeQ(Ez(KJkRI zY>9?XR!@BTwVFEvs@K5Cm1nY<31nP0lI9&V$3!ruoDnyHQ(qpjC+Cp(pV!p!0 zMglWe%~S^UfMg6@=~Lg$Q{xUr%@Rol2{*ZAGF_%%q_G|K=$nms4+|5PZ_ga(RVU)K zRb0I&@iEA_rCb@iD(-VD@!o>;M|78dhHBBH_c`w&mLb*W#@dAzgJQXyI|4MU*~+O9 zFY|n^F;l8Xf%+bOGhicTHPLUcR^{M|tkRUgk3ZpGV$nI_FhF!y$Xi-TBEtSMy+5BZ zdL~6FZDKj{@~8J|+23J)fKv%89y0tDU^aV^Z!Q?D$4A&3$Q7}iG;_|vo_9VR22vWT zhLb;(z1E)3J63vG5BzRs=0DP=4|JIk*2fhRsOJ>?fZD0NqdK+VjC&}KAuRC?#Lw=bINR4H*%S8J^4s!qDQLLW@0ACP&06bkh|Jwh8OD3Pa2J+7| VV@d{}^Z%8A1IERs?u=jZe*rQ-_iq3I literal 0 HcmV?d00001