PSMake.psm1
# Import System.Security from GAC if Windows PowerShell is being used if ( ([version]$PSVersionTable.PSVersion).Major -lt 6) { $asm = [System.Reflection.Assembly]::LoadWithPartialName('System.Security') if ($null -eq $asm) { throw 'Unable to load System.Security from GAC for Windows PowerShell' } } . ([scriptblock]::Create(@' function AddType { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Base64Encode', Justification='Used in ForEach-Object call')] param( [scriptblock]$Scriptblock, [string]$OutputDirectory = $settings.OutputDirectory, [string]$Filename = $settings.ModuleName + ".psm1", [switch]$Base64Encode ) $content = & $Scriptblock | ForEach-Object { $current = $_ if($current -is [string]) { Get-ChildItem $current } elseif($current -is [System.IO.FileInfo]) { $current } else { throw "Invalid object to collate - $_ of type '$($_.GetType())" } } | ForEach-Object { $content = [System.IO.File]::ReadAllText($_.FullName) $scriptblockText = $content if($Base64Encode) { $encodedScriptblock = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($scriptblockText)) "Add-Type -TypeDefinition ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('$encodedScriptblock')))" } else { "Add-Type -TypeDefinition @'`r`n$scriptblockText`r`n'@`r`n" } } if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null } if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null } $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append } '@)) . ([scriptblock]::Create(@' using namespace System.Security.Cryptography.X509Certificates using namespace System.IO function CodeSign { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", 'BaseDirectory', Justification = "Used in ForEach-Object scriptblock")] Param( [ScriptBlock]$ScriptBlock, [string]$BaseDirectory = $settings.OutputModulePath, [string]$CertificatePath = 'Cert:\CurrentUser\My', [string]$TimestampServer = 'http://timestamp.comodoca.com/authenticode' ) [X509Certificate2[]] $signingCerts = Get-ChildItem $CertificatePath -Recurse | Where-Object { $_ -is [X509Certificate2] -and ($_.EnhancedKeyUsageList.FriendlyName) -contains 'Code Signing' } if(-not $signingCerts) { throw "No Code Signing Certs Found!"} [X509Certificate2Collection]$selection = [X509Certificate2UI]::SelectFromCollection($signingCerts, "Select Certificate", "Select Code Signing Certificate", [X509SelectionFlag]::SingleSelection) if($selection.Count -ne 1) { throw 'No Code Signing Certificate Selected!' } [X509Certificate2]$cert = $selection[0] $authenticodeArgs = @{ "Certificate" = $cert "IncludeChain" = 'all' } if($TimestampServer) { $authenticodeArgs["TimestampServer"] = $TimestampServer } $files = & $ScriptBlock | ForEach-Object { if($_ -is [string]) { Get-ChildItem (Resolve-Path (Join-Path $BaseDirectory $_)) -File } elseif($_ -is [FileInfo]) { $_ } else { throw "Item $_ neither a path or file. Cannot Code Sign" } } $authenticodeArgs["FilePath"] = $files.FullName Set-AuthenticodeSignature @authenticodeArgs | Out-Null } '@)) . ([scriptblock]::Create(@' function Collate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", 'Base64Encode', Justification = "Used in ForEach-Object scriptblock")] param( [scriptblock]$Scriptblock, [string]$OutputDirectory = $settings.OutputDirectory, [string]$Filename = $settings.ModuleName + ".psm1", [switch]$Base64Encode ) $content = & $Scriptblock | ForEach-Object { $current = $_ if($current -is [string]) { Get-ChildItem $current } elseif($current -is [System.IO.FileInfo]) { $current } else { throw "Invalid object to collate - $_ of type '$($_.GetType())" } } | ForEach-Object { $content = [System.IO.File]::ReadAllText($_.FullName) $scriptblockText = $content if($Base64Encode) { $encodedScriptblock = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($scriptblockText)) ". ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('$encodedScriptblock'))))" } else { ". ([scriptblock]::Create(@'`r`n$scriptblockText`r`n'@))`r`n" } } if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null } if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null } $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append } '@)) . ([scriptblock]::Create(@' using namespace System.IO; function CopyDirectory { Param( [ScriptBlock]$ScriptBlock, [string]$To = $settings.OutputModulePath ) if($To -ne $settings.OutputModulePath -and -not (Split-Path $To -IsAbsolute)) { $To = Join-Path $settings.OutputModulePath $To } & $ScriptBlock | ForEach-Object { if($_ -is [string]) { if(Split-Path $_ -IsAbsolute) { $_ } else { (Join-Path $PWD.Path $_) } } elseif($_ -is [DirectoryInfo]) { $_.FullName } else { throw "Unexpected directory to copy - '$_'" } } | ForEach-Object { Copy-Item $_ $To -Recurse } } '@)) . ([scriptblock]::Create(@' using namespace System.IO function CopyFiles { param( [scriptblock]$ScriptBlock, [string]$To = $settings.OutputModulePath ) if($To -ne $settings.OutputModulePath -and -not (Split-Path $To -IsAbsolute)) { $To = Join-Path $settings.OutputModulePath $To } & $ScriptBlock | ForEach-Object { if($_ -is [string]) { Get-ChildItem $_ } elseif($_ -is [FileInfo]) { $_ } else { throw "Unexpected item to copy - '$_'" } } | ForEach-Object { Copy-Item $_.FullName $To } } '@)) . ([scriptblock]::Create(@' using namespace System.IO function CreateDirectory { param( [scriptblock]$ScriptBlock, [string]$In = $settings.OutputModulePath ) $fullIn = (Resolve-Path $In).Path & $ScriptBlock | ForEach-Object { if($_ -is [string]) { [DirectoryInfo]::new([Path]::Combine($fullIn, $_)) } elseif($_ -is [System.IO.DirectoryInfo]) { $_ } else { throw "Unexpected item to copy - '$_'" } } | ForEach-Object { if(-not $_.Exists) { $_.Create() } } } '@)) . ([scriptblock]::Create(@' function CustomCode { param( [scriptblock]$Scriptblock, [string]$OutputDirectory = $settings.OutputDirectory, [string]$Filename = $settings.ModuleName + ".psm1" ) $content = $Scriptblock.ToString() if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null } if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null } $content | Out-File (Join-Path $settings.OutputModulePath $Filename) -Append } '@)) . ([scriptblock]::Create(@' function Debug { Param( [scriptblock]$ScriptBlock, [string]$BuildTarget = $settings.BuildTarget ) if($BuildTarget -eq "Debug") { & $ScriptBlock } } '@)) . ([scriptblock]::Create(@' [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'BuildSetting is weird and this is used internally only')] param() function Get-BuildSettings { [OutputType([Hashtable])] param( [string]$BuildFilePath, [string]$BuildTarget ) if(-not (test-path $BuildFilePath)) { throw "No $($BuildFilePath.Substring(2)) file found!" } $defaultData = Import-PowerShellDataFile (Join-Path $MyInvocation.MyCommand.Module.ModuleBase "defaultsettings.psd1") $buildSettings = @{} $buildSettingsAsConfigured = Import-PowerShellDataFile $BuildFilePath foreach($defaultKey in $defaultData.Keys) { $buildSettings.Add($defaultkey, $defaultData[$defaultKey]) } foreach($asConfiguredKey in $buildSettingsAsConfigured.Keys) { $buildSettings[$asConfiguredKey] = $buildSettingsAsConfigured[$asConfiguredKey] } $buildSettings["BuildTarget"] = if($PSBoundParameters.ContainsKey("BuildTarget")) { $BuildTarget } elseif($buildSettings.ContainsKey("DefaultBuildTarget")) { $buildSettings["DefaultBuildTarget"] } else { "Release" } $buildSettings["BuildTargetPath"] = Join-Path $buildSettings["OutputDirectory"] $buildSettings["BuildTarget"] if(-not $buildSettingsAsConfigured.ContainsKey("OutputModulePath")) { $buildSettings.Add("OutputModulePath", (Join-Path $buildSettings["BuildTargetPath"] $buildSettings["ModuleName"])) } $credential = GetBuildCredential -Settings $buildSettings Write-Verbose "Credential - $credential" if ($credential) { $buildSettings.Credential = $credential } # Check for required parameters Validate-BuildSettings $buildSettings $buildSettings } '@)) . ([scriptblock]::Create(@' using namespace System.IO function GetBuildCredential { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConverttoSecureStringWithPlainText', '', Justification = '-AsPlainText is required on *nix systems')] [CmdletBinding()] [OutputType([PSCredential])] param( $Settings = $settings ) # Environment wins ALWAYS if ($env:POWERSHELL_REPO_USR -and $env:POWERSHELL_REPO_PW) { return [pscredential]::new($env:POWERSHELL_REPO_USR, (ConvertTo-SecureString -String $env:POWERSHELL_REPO_PW -AsPlainText -Force)) } # Custom set Credential if($null -ne $Settings.Credential) { if ($Settings.Credential -is [scriptblock]) { # CustomFactory Write-Verbose "Credential is a scriptblock, Invoking now." $output = $Settings.Credential.InvokeWithContext($null, [psvariable]::new("settings", $Settings), $null) if (-not $output -or -not ($output -is [pscredential])) { throw "Credential Factory did not return a PSCredential object!" } return $output } elseif ($Settings.Credential -is [string]) { Write-Verbose 'Credential is a string. Checking path.' # Configured Path if (-not (test-path $Settings.Credential -PathType Leaf)) { throw "Credential path '$($Settings.Credential)' does not exist or is not a file!" } [FileInfo]$fileInfo = [FileInfo]::new($Settings.Credential) $obj = switch($fileInfo.Extension.ToLower()) { ".json" { Write-Verbose 'JSON credential detected.' Get-Content $Settings.Credential -Raw | ConvertFrom-Json -ErrorAction Stop } ".xml" { Write-Verbose 'CliXml credential detected.' Import-CliXml $Settings.Credential -ErrorAction Stop } default { throw "Credential in file '$($Settings.Credential)' not in json or xml format!" } } $usernameProperty = $obj.PSObject.Properties.Match("UserName") $passwordProperty = $obj.PSObject.Properties.Match("Password") if ($null -eq $usernameProperty -or $null -eq $passwordProperty) { throw "Credential in file '$($Settings.Credential)' missing required username and password properties!" } return [pscredential]::new($usernameProperty.Value, (ConvertTo-SecureString -String $passwordProperty.Value -AsPlainText -Force)) } elseif ($Settings.Credential -is [PSCredential]) { Write-Verbose 'Credential property is already a PSCredential.' return $Settings.Credential } else { throw 'Restore Credential is not a factory, file path, or PSCredential!' } } Write-Verbose 'No credential detected. Returning null.' # Default - $null return $null } '@)) . ([scriptblock]::Create(@' function GetPlatformEnvironment { [CmdletBinding()] param() [System.OperatingSystem]$OS = [System.Environment]::OSVersion return [PSCustomObject]@{ Platform = $OS.Platform OSVersion = [Version]($OS.Version) OSVersionString = $OS.VersionString PSVersion = [Version]($PSVersionTable.PSVersion) } } '@)) . ([scriptblock]::Create(@' function GetRestoredDependency { param( [Parameter(Mandatory = $true)] [string]$ModuleName, [string]$ModuleVersion, [string]$RequiredVersion, [Parameter(Mandatory = $true)] [string]$PSModuleDirectory ) if (-not (Test-Path $PSModuleDirectory -PathType Container)) { throw "PSModuleDirectory path '$PSModuleDirectory' does not exist or is not a folder." } $requiredVersion = $null if($RequiredVersion) { [Version]::TryParse($RequiredVersion, [ref]$requiredVersion) | Out-Null } $moduleVersion = $null if ($ModuleVersion) { [Version]::TryParse($ModuleVersion, [ref]$moduleVersion) | Out-Null } Get-ChildItem -Path (Join-Path $PSModuleDirectory $ModuleName) -Directory -ErrorAction SilentlyContinue | ` Where-Object { $Version = $null if ([Version]::TryParse($_.Name, [ref]$Version)) { if($requiredVersion) { $Version -eq $requiredVersion } elseif ($moduleVersion) { $Version -ge $moduleVersion -and $Version.Major -eq $moduleVersion.Major } else { $true } } else { $false } } | ` Sort-Object -Descending -Property "Name" | ` Select-Object -First 1 | ` Select-Object -ExpandProperty FullName | ` ForEach-Object { Import-PowerShellDataFile -Path (Join-Path $_ "$ModuleName.psd1") -ErrorAction SilentlyContinue | ` ForEach-Object { $_.Add("Name", $ModuleName) $_.Add("Version", $_.ModuleVersion) Write-Output $_ } } } '@)) . ([scriptblock]::Create(@' using namespace System.Management.Automation using namespace System.Collections.ObjectModel using namespace System function Invoke-PSMake { [CmdletBinding()] param( [ValidateSet("","build", "clean", "test", "template", "publish")] [string]$Command = "build" ) DynamicParam { if ($Command -eq 'template') { $paramDictionary = [RuntimeDefinedParameterDictionary]::new() $attributeCollection = [Collection[Attribute]]::new() $projectNameParameterAttribute = [ParameterAttribute]@{ Mandatory = $true Position = 2 } $attributeCollection.Add($projectNameParameterAttribute) $projectNameParam = [RuntimeDefinedParameter]::new('ProjectName', [string], $attributeCollection) $paramDictionary.Add("ProjectName", $projectNameParam) return $paramDictionary } if ($Command -eq 'publish') { $paramDictionary = [RuntimeDefinedParameterDictionary]::new() $attributeCollection = [Collection[Attribute]]::new() $nugetApiKeyAttribute = [ParameterAttribute]@{ Position = 2 } $attributeCollection.Add($nugetApiKeyAttribute) $nugetApiKeyParam = [RuntimeDefinedParameter]::new('NuGetApiKey', [string], $attributeCollection) $paramDictionary.Add("NuGetApiKey", $nugetApiKeyParam) $buildTargetAttributeCollection = [Collection[Attribute]]::new() $buildTargetParameterAttribute = [ParameterAttribute]@{ Position = 3 } $buildTargetAttributeCollection.Add($buildTargetParameterAttribute) $buildTargetParam = [RuntimeDefinedParameter]::new('BuildTarget', [string], $buildTargetAttributeCollection) $buildTargetParam.Value = "Release" $PSBoundParameters["BuildTarget"] = $buildTargetParam.Value $paramDictionary.Add("BuildTarget", $buildTargetParam) return $paramDictionary } if ($Command -eq 'test') { $paramDictionary = [RuntimeDefinedParameterDictionary]::new() $attributeCollection = [Collection[Attribute]]::new() $reportsParameterAttribute = [ParameterAttribute]@{ Position = 2 } $attributeCollection.Add($reportsParameterAttribute) $reportsParam = [RuntimeDefinedParameter]::new('ReportType', [string], $attributeCollection) $paramDictionary.Add("ReportType", $reportsParam) return $paramDictionary } if ($Command -eq 'build') { $paramDictionary = [RuntimeDefinedParameterDictionary]::new() $attributeCollection = [Collection[Attribute]]::new() $buildTargetParameterAttribute = [ParameterAttribute]@{ Position = 2 } $attributeCollection.Add($buildTargetParameterAttribute) $buildTargetParam = [RuntimeDefinedParameter]::new('BuildTarget', [string], $attributeCollection) $buildTargetParam.Value = "Release" $PSBoundParameters["BuildTarget"] = $buildTargetParam.Value $paramDictionary.Add("BuildTarget", $buildTargetParam) return $paramDictionary } } Begin { $invokeArgs = @() } Process{ $PSBoundParameters.Keys | ForEach-Object { Write-Verbose "Bound Parameter: $_ = $($PSBoundParameters[$_])" } switch($Command) { "template" { $path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PSBoundParameters["ProjectName"]) $name = Split-Path $path -Leaf Write-Verbose "Project Directory: $path" Write-Verbose "Project Name: $name" if (-not (Test-Path $path -PathType Container)) { Write-Verbose "Creating $path" New-Item -Path $path -ItemType Directory -Force | Out-Null } if (-not (test-path (Join-Path $path "functions") -PathType Container)) { Write-Verbose "Creating $(Join-Path $path "functions")" New-Item -Path (Join-Path $path "functions") -ItemType Directory -Force | Out-Null } if (-not (test-path (Join-Path $path "tests") -PathType Container)) { Write-Verbose "Creating $(Join-Path $path "tests")" New-Item -Path (Join-Path $path "tests") -ItemType Directory | Out-Null } Write-Verbose "Generating $(Join-Path $path "$name.psm1")" 'Get-ChildItem $PSScriptRoot\functions\ -File -Recurse | ForEach-Object { . $_.FullName }' | Out-File (Join-Path $path "$name.psm1") -Encoding ASCII $dom = $env:userdomain $usr = $env:username try { Write-Verbose "Getting user full name from active directory - User - $usr, Domain -$dom" $author = ([adsi]"WinNT://$dom/$usr,user").fullname.ToString() } catch { $author = "$usr$(if($dom) { "@$dom" })" Write-Verbose "Failed, using author '$author'" } if(-not (test-path (Join-Path $path "$name.psd1") -PathType Leaf)) { Write-Verbose "Generating manifest $(Join-Path $path "$name.psd1")" New-ModuleManifest -Path (Join-Path $path "$name.psd1") -CompanyName "USAF, 38 CEIG/ES" -Copyright "GOTS" -RootModule "$name.psm1" -ModuleVersion "1.0.0.0" -Author $author } Write-Verbose "Generating $(Join-Path $path "build.psd1")" (Get-Content "$($MyInvocation.MyCommand.Module.ModuleBase)\template.psd1").Replace("%%MODULENAME%%", $name) | Out-File (Join-Path $path "build.psd1") Write-Verbose "Copying Pester5Configuration-local.psd1 and Pester5Configuration-cicd.psd1" Copy-Item "$($MyInvocation.MyCommand.Module.ModuleBase)\Pester5Configuration-local.psd1" (Join-Path $path "Pester5Configuration-local.psd1") Copy-Item "$($MyInvocation.MyCommand.Module.ModuleBase)\Pester5Configuration-cicd.psd1" (Join-Path $path "Pester5Configuration-cicd.psd1") } {$_ -ne "template" } { $buildArgs = @{ BuildFilePath = ".\build.psd1" } if($PSBoundParameters.ContainsKey("BuildTarget")) { $buildArgs.Add("BuildTarget", $PSBoundParameters["BuildTarget"]) } $buildData = Get-BuildSettings @buildArgs $settings = @{} $buildData.Keys | Where-Object { -not ($_ -in "build","clean","test","template","publish") } | ForEach-Object { $settings[$_] = $buildData[$_] } } { $_ -in "","build" } { if(-not (test-path $buildData.OutputDirectory -PathType Container)) { New-Item $buildData.OutputDirectory -ItemType Directory | Out-Null } if(-not (test-path $buildData.OutputModulePath -PathType Container)) { New-Item $buildData.OutputModulePath -ItemType Directory | Out-Null } $invokeArgs += $PSBoundParameters["BuildTarget"] } { $_ -in "publish", "test"} { ${function:RestoreDependencies}.InvokeWithContext($null, [PSVariable]::new("settings", $settings), $null) } { $_ -eq 'publish' } { if($PSBoundParameters.ContainsKey("NuGetApiKey")) { $invokeArgs += $PSBoundParameters["NuGetApiKey"] } } { $_ -eq "test" } { if($PSBoundParameters.ContainsKey("ReportType")) { $invokeArgs += $PSBoundParameters["ReportType"] } } {$_ -in "", "build","clean","test","publish" } { if(-not $buildData.ContainsKey($Command)) { throw "Unable to run '$Command' due to build.psd1 not containing a '$Command' scriptblock" } if($buildData.ContainsKey("DevRequiredModules")) { RestoreDependencies -RequiredModule $buildData["DevRequiredModules"] } InvokeWithPSModulePath -NewPSModulePath $settings.RestoredDependenciesPath -ScriptBlock { (& $buildData[$Command]).InvokeWithContext($null, [PSVariable]::new('settings', $settings), $invokeArgs) }.GetNewClosure() } default { throw "Undefined command '$Command'" } } } <# .SYNOPSIS Invokes the PSMake project management tool based on given parameter (defaults to build release) .DESCRIPTION Builds a PSMake structured project in the current directory based on the build.psd1 file (build, test, clean, plublish) or creates the project structure with default settings (template) .PARAMETER Command Specifies the action to take (build, test, clean, publish, template) .INPUTS None. Piping unavailable. .OUTPUTS None. Affects project outputs and runs other test scripts based on the build.psd1 file. .EXAMPLE PS> PSMake # builds a release version of the module specified within build.psd1 PS> PSMake build release # same as above, but explicit .EXAMPLE PS> PSMake build debug # builds the debug version of the module as specified within the build.psd1 Build property-script .EXAMPLE PS> PSMake clean # runs the 'Clean' property-script within build.psd1 (deletes the dist/ folder by default) .EXAMPLE PS> PSMake test # runs the 'Test' property-script within the build.psd1 file PS> PSMake test reports # runs the 'Test' property-script within the build.psd1 file and passes "reports" value as a parameter to it. .EXAMPLE PS> PSMake publish # runs the 'Publish' property-script within the build.psd1 file .EXAMPLE PS> PSMake template # Initializes a new PSMake project with templated build.psd1, module file, module manifest, specialized folders #> } New-Alias -Name "psmake" Invoke-PSMake -ErrorAction SilentlyContinue Export-ModuleMember -Function 'Invoke-PSMake' Export-ModuleMember -Alias "psmake" '@)) . ([scriptblock]::Create(@' using namespace System.IO function InvokeWithPSModulePath { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ScriptBlock', Justification = 'ScriptBlock is used within a child scriptblock')] [CmdletBinding()] param( [string]$NewPSModulePath, [ScriptBlock]$ScriptBlock ) $pe = GetPlatformEnvironment $seperatorChar = switch($pe.Platform) { { $_ -in [System.PlatformID]::MacOSX, [System.PlatformID]::Unix } { ':' } default { ";" } } $cache = $env:PSModulePath $newList = @() $newList += ([System.IO.Path]::GetFullPath($NewPSModulePath)) $cache.Split($seperatorChar) | ForEach-Object { $newList += $_ } $env:PSModulePath = [string]::Join($seperatorChar, $newList) Write-Verbose "PSModulePath = $($env:PSModulePath)" $ps = [PowerShell]::Create([System.Management.Automation.RunspaceMode]::CurrentRunspace) try { $ps.AddScript("`$env:PSModulePath='$($env:PSModulePath)'") | Out-Null $ps.AddCommand("Invoke-Command") | Out-Null $ps.AddParameter("ScriptBlock", { . $MyInvocation.MyCommand.Module $ScriptBlock }.GetNewClosure()) | Out-Null $ps.Invoke() if($ps.HadErrors) { $ps.Streams.Error | ForEach-Object { Write-Error $_; $_.InvocationInfo.PositionMessage } } } finally { $ps.Dispose() $env:PSModulePath = $cache } } '@)) . ([scriptblock]::Create(@' function Prerelease { Param( [scriptblock]$ScriptBlock, [string]$BuildTarget = $settings.BuildTarget ) if($BuildTarget -eq "Prerelease") { & $ScriptBlock } } '@)) . ([scriptblock]::Create(@' function Release { Param( [scriptblock]$ScriptBlock, [switch]$AndPrerelease, [string]$BuildTarget = $settings.BuildTarget ) if(($PSBoundParameters.ContainsKey("AndPrerelease") -and $BuildTarget -in "Release", "Prerelease") -or $BuildTarget -eq "Release") { & $ScriptBlock } } '@)) . ([scriptblock]::Create(@' using namespace System.IO function RestoreDependencies { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AllowPrerelease', Justification = 'Used in a ForEach-Object scriptblock')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Credential', Justification = 'Used in a ForEach-Object scriptblock')] [CmdletBinding()] param( $RequiredModules = (Import-PowerShellDataFile "$($settings.ModuleName).psd1").RequiredModules, $OutputDirectory = ".dependencies", $AllowPrerelease = $false, [pscredential]$Credential = $settings.Credential ) if (-not $RequiredModules) { return } if (-not (test-path $OutputDirectory)) { new-item -Path $OutputDirectory -ItemType Directory | Out-Null } # Ensure dependencies are installed before importing the module Write-Verbose "Restoring Dependencies..." $RequiredModules | ForEach-Object { $module = $_ $moduleInfo = @{} if ($module -is [string]) { $moduleInfo.Add("Name", $module) } else { $moduleInfo.Add("Name", $module.ModuleName) if($module.ContainsKey("ModuleVersion")) { $moduleInfo.Add("MinimumVersion", $module.ModuleVersion) } elseif ($module.ContainsKey("RequiredVersion")) { $moduleInfo.Add("RequiredVersion", $module.RequiredVersion) } } if ($PSBoundParameters.ContainsKey("AllowPrerelease")) { $moduleInfo.Add("AllowPrerelease", $AllowPrerelease) } $moduleInfo.Add("ErrorAction", "Stop") if($Credential) { $moduleInfo.Add("Credential", $Credential) } $cachedModuleInfo = @{ PSModuleDirectory = $OutputDirectory } $module.Keys | ForEach-Object { $cachedModuleInfo.Add($_, $module[$_]) } $foundModule = GetRestoredDependency @cachedModuleInfo if ($null -eq $foundModule) { Write-Verbose "Restoring Module '$($moduleInfo.Name)'$(if($moduleInfo.RequiredVersion) { ", RequiredVersion = $($moduleInfo.RequiredVersion)"})$(if($moduleInfo.MinimumVersion) { ", MinimumVersion = $($moduleInfo.MinimumVersion)"})$(if($Credential) { " using username $($Credential.UserName)" })" $foundModule = Find-Module @moduleInfo | Select-Object -First 1 $installedModulePath = [Path]::Combine($OutputDirectory, $foundModule.Name, $foundModule.Version.Split('-')[0]) $installedModuleInfoPath = [Path]::Combine($installedModulePath, 'PSGetModuleInfo.xml') if (-not (test-path $installedModuleInfoPath) -or $foundModule.Version -ne ((Import-CliXml $installedModulePath\PSGetModuleInfo.xml)).Version) { Write-Verbose "Module $($foundModule.Name) ($($foundModule.Version)) not installed... installing." $SaveArgs = @{ Name = $foundModule.Name Path = $OutputDirectory RequiredVersion = $foundModule.Version.ToString() Repository = $foundModule.Repository ErrorAction = 'Stop' Force = $true } if ($AllowPrerelease) { $SaveArgs.Add("AllowPrerelease", $AllowPrerelease) } if ($Credential) { $SaveArgs.Add("Credential", $Credential) } Save-Module @SaveArgs } } Write-Verbose "Using Module - $($foundModule.Name) $($foundModule.Version)" } } '@)) . ([scriptblock]::Create(@' function SetPrereleaseTag { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Tag', Justification = 'Used in a ForEach-Object scriptblock')] Param( [scriptblock]$ScriptBlock, [string]$Tag = "rc$((Get-Date).ToString("yyyyMMddHHmm"))" ) & $ScriptBlock | ForEach-Object { $path = Join-Path $settings.OutputModulePath $_ if (-not (Test-Path $path -PathType Leaf)) { throw "Path '$_' is not file" } Update-Metadata $path -PropertyName "PrivateData.PSData.Prerelease" -Value $Tag } } '@)) . ([scriptblock]::Create(@' function UsingModule { param( [scriptblock]$Scriptblock, [string]$OutputDirectory = $settings.OutputDirectory, [string]$Filename = $settings.ModuleName + ".psm1" ) $content = & $Scriptblock | ForEach-Object { $current = $_ if($current -is [string]) { $current } else { throw "Invalid object - expected string of module to use - $_ of type '$($_.GetType())" } } | ForEach-Object { "using module `"$_`"`r`n" } if(-not (Test-Path $OutputDirectory -PathType Container)) { New-Item $OutputDirectory -ItemType Directory | Out-Null } if(-not (Test-Path $settings.OutputModulePath -PathType Container)) { New-Item $settings.OutputModulePath -ItemType Directory | Out-Null } $content + (Get-Content (Join-Path $settings.OutputModulePath $Filename) -ErrorAction SilentlyContinue) | Out-File (Join-Path $settings.OutputModulePath $Filename) } '@)) . ([scriptblock]::Create(@' [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Scope='Function', Justification = 'BuildSettings is a type')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Scope='Function', Justification = 'BuildSettings is a type')] param() function Validate-BuildSettings { param( [hashtable]$Settings ) # Check for module name if(-not $Settings.ContainsKey("ModuleName")) { throw "Required Property 'ModuleName' is not defined." } if(-not ($Settings["ModuleName"] -match "^[a-zA-Z][a-zA-Z0-9-_]*$")) { throw "Property 'ModuleName' ($($Settings["ModuleName"])) is invalid." } # Check for Build scriptblock if(-not $Settings.ContainsKey("Build")) { throw "Required Property 'Build' is not defined" } if(-not $Settings["Build"] -is [scriptblock]) { throw "Property Build is not a scriptblock! ($($Settings.Build))" } # Check for Build scriptblock if(-not $Settings.ContainsKey("Clean")) { throw "Required Property 'Clean' is not defined" } if(-not $Settings["Clean"] -is [scriptblock]) { throw "Property Clean is not a scriptblock! ($($Settings.Clean))" } # Check if Valid BuildTarget if(-not $Settings.ContainsKey("BuildTarget")) { throw "Required Property 'BuildTarget' is not defined" } if(-not $Settings["BuildTarget"] -is [string]) { throw "Property 'BuildTarget' not a string! ($($Settings.BuildTarget))" } if(-not @("Release", "Prerelease", "Debug") -contains $Settings["BuildTarget"]) { throw "Property 'BuildTarget' is not a valid build target (Release, Debug)! ($($Settings.BuildTarget))" } } '@)) |