ModuleBuilder.psm1

#Region '.\Private\CopyReadme.ps1' 0
function CopyReadme {
    [CmdletBinding()]
    param(
        # The path to the ReadMe document to copy
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()][AllowEmptyString()]
        [string]$ReadMe,

        # The name of the module -- because the file is renamed to about_$ModuleName.help.txt
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [Alias("Name")]
        [string]$ModuleName,

        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [string]$OutputDirectory,

        # The culture (language) to store the ReadMe as (defaults to "en")
        [Parameter(ValueFromPipelineByPropertyName)]
        [Globalization.CultureInfo]$Culture = $(Get-UICulture),

        # If set, overwrite the existing readme
        [Switch]$Force
    )

    # Copy the readme file as an about_ help file
    Write-Verbose "Test for ReadMe: $Pwd\$($ReadMe)"
    if($ReadMe -and (Test-Path $ReadMe -PathType Leaf)) {
        # Make sure there's a language path
        $LanguagePath = Join-Path $OutputDirectory $Culture
        if(!(Test-Path $LanguagePath -PathType Container)) {
            $null = New-Item $LanguagePath -Type Directory -Force
        }
        Write-Verbose "Copy ReadMe to: $LanguagePath"

        $about_module = Join-Path $LanguagePath "about_$($ModuleName).help.txt"
        if(!(Test-Path $about_module)) {
            Write-Verbose "Turn readme into about_module"
            Copy-Item -LiteralPath $ReadMe -Destination $about_module
        }
    }
}
#EndRegion '.\Private\CopyReadme.ps1' 41
#Region '.\Private\InitializeBuild.ps1' 0
function InitializeBuild {
    <#
        .SYNOPSIS
            Loads build.psd1 and the module manifest and combines them with the parameter values of the calling function. Pushes location to the module source location.
        .DESCRIPTION
            This function is for internal use from Build-Module only
            It does two things that make it really only work properly there:
 
            1. It calls Push-Location without Pop-Location to push the SourcePath into the "Build-Module" stack
            2. It reads the ParameterValues from the PARENT MyInvocation
        .NOTES
            Depends on the internal ResolveModuleSource and ResolveModuleManifest
            Depends on the Configuration module Update-Object and (the built in Import-LocalizedData and Get-Module)
    #>

    [CmdletBinding()]
    param(
        # The root folder where the module source is (including the Build.psd1 and the module Manifest.psd1)
        [string]$SourcePath,

        # Pass the invocation from the parent in, so InitializeBuild can read parameter values
        [Parameter(DontShow)]
        $Invocation = $(Get-Variable MyInvocation -Scope 1 -ValueOnly)
    )
    # NOTE: This reads the parameter values from Build-Module!
    # BUG BUG: needs to prioritize build.psd1 values over build-module *defaults*, but user-provided parameters over build.psd1 values
    Write-Debug "Initializing build variables"

    $ModuleSource = ResolveModuleSource $SourcePath
    Push-Location $ModuleSource -StackName Build-Module

    # These errors are caused by trying to parse valid module manifests without compiling the module first
    $ErrorsWeIgnore = "^" + @(
        "Modules_InvalidRequiredModulesinModuleManifest"
        "Modules_InvalidRootModuleInModuleManifest"
    ) -join "|^"

    # Read a build.psd1 configuration file for default parameter values
    $BuildInfo = Import-Metadata -Path (Join-Path $ModuleSource [Bb]uild.psd1)
    # Combine the defaults with parameter values
    $ParameterValues = @{}
    foreach ($parameter in $Invocation.MyCommand.Parameters.GetEnumerator()) {
        $key = $parameter.Key
        # set if it doesn't exist, overwrite if the value is bound as a parameter
        if (!$BuildInfo.ContainsKey($key) -or ($Invocation.BoundParameters -and $Invocation.BoundParameters.ContainsKey($key))) {
            if ($null -ne ($value = Get-Variable -Name $key -ValueOnly -ErrorAction Ignore )) {
                if ($value -ne ($null -as $parameter.Value.ParameterType)) {
                    Write-Debug " $key = $value"
                    $ParameterValues[$key] = $value
                }
            }
        }
    }

    $BuildInfo = $BuildInfo | Update-Object $ParameterValues

    # Make sure the Path is set and points at the actual manifest
    $BuildInfo.Path = ResolveModuleManifest $ModuleSource $BuildInfo.Path

    # Finally, add all the information in the module manifest to the return object
    $ModuleInfo = Get-Module $BuildInfo.Path -ListAvailable -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable Problems

    # If there are any problems that count, fail
    if ($Problems = $Problems.Where({$_.FullyQualifiedErrorId -notmatch $ErrorsWeIgnore})) {
        foreach ($problem in $Problems) {
            Write-Error $problem
        }
        throw "Unresolvable problems in module manifest"
    }

    # Update the ModuleManifest with our build configuration
    $ModuleInfo = Update-Object -InputObject $ModuleInfo -UpdateObject $BuildInfo

    # Ensure the OutputDirectory makes sense (it's never blank anymore)
    if (![IO.Path]::IsPathRooted($ModuleInfo.OutputDirectory)) {
        # Relative paths are relative to the build.psd1 now
        $OutputDirectory = Join-Path $ModuleSource $ModuleInfo.OutputDirectory
        $ModuleInfo.OutputDirectory = $OutputDirectory
    }

    $ModuleInfo
}
#EndRegion '.\Private\InitializeBuild.ps1' 81
#Region '.\Private\ParameterValues.ps1' 0
Update-TypeData -TypeName System.Management.Automation.InvocationInfo -MemberName ParameterValues -MemberType ScriptProperty -Value {
    $results = @{}
    foreach ($key in $this.MyCommand.Parameters.Keys) {
        if ($this.BoundParameters.ContainsKey($key)) {
            $results.$key = $this.BoundParameters.$key
        } elseif ($value = Get-Variable -Name $key -Scope 1 -ValueOnly -ErrorAction Ignore) {
            $results.$key = $value
        }
    }
    return $results
} -Force
#EndRegion '.\Private\ParameterValues.ps1' 11
#Region '.\Private\ParseLineNumber.ps1' 0
function ParseLineNumber {
    <#
        .SYNOPSIS
            Parses the SourceFile and SourceLineNumber from a position message
        .DESCRIPTION
            Parses messages like:
                at <ScriptBlock>, <No file>: line 1
                at C:\Test\Path\ErrorMaker.ps1:31 char:1
                at C:\Test\Path\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4
    #>

    [Cmdletbinding()]
    param(
        # A position message, starting with "at ..." and containing a line number
        [Parameter(ValueFromPipeline)]
        [string]$PositionMessage
    )
    process {
        foreach($line in $PositionMessage -split "\r?\n") {
            # At (optional invocation,) <source file>:(maybe " line ") number
            if ($line -match "at(?: (?<InvocationBlock>[^,]+),)?\s+(?<SourceFile>.+):(?<!char:)(?: line )?(?<SourceLineNumber>\d+)(?: char:(?<OffsetInLine>\d+))?") {
                [PSCustomObject]@{
                    PSTypeName       = "Position"
                    SourceFile       = $matches.SourceFile
                    SourceLineNumber = $matches.SourceLineNumber
                    OffsetInLine     = $matches.OffsetInLine
                    PositionMessage  = $line
                    PSScriptRoot     = Split-Path $matches.SourceFile
                    PSCommandPath    = $matches.SourceFile
                    InvocationBlock  = $matches.InvocationBlock
                }
            } elseif($line -notmatch "\s*\+") {
                Write-Warning "Can't match: '$line'"
            }
        }
    }
}
#EndRegion '.\Private\ParseLineNumber.ps1' 36
#Region '.\Private\ResolveModuleManifest.ps1' 0
function ResolveModuleManifest {
    <#
        .Synopsis
            Resolve the module manifest path in the module source base.
    #>

    [OutputType([string])]
    param(
        # The path to the module folder, manifest or build.psd1
        [Parameter(Position = 0, ValueFromPipelineByPropertyName)]
        [ValidateScript( {
                if (Test-Path $_ -PathType Container) {
                    $true
                } else {
                    throw "ModuleBase must point to the source base for a module: $_"
                }
            })]
        [Alias("ModuleManifest")]
        [string]$ModuleBase = $(Get-Location -PSProvider FileSystem),

        [Parameter(Position = 1, ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [Alias("ModuleName")]
        [string]$Name
    )
    Push-Location $ModuleBase -StackName ResolveModuleManifest

    if(!$PSBoundParameters.ContainsKey("Name") -or !$Name) {
        # Do not use GetFileNameWithoutExtension, because some module names have dots in them
        $Name = (Split-Path $ModuleBase -Leaf) -replace "\.psd1$"
        # If we're in a "well known" source folder, look higher for a name
        if ($Name -in "Source", "src") {
            $Name = Split-Path (Split-Path $ModuleBase) -Leaf
        }

        # If the folder name isn't the manifest name, look in the build.psd1
        if (!(Test-Path "$Name.psd1") -and (Test-Path "build.psd1")) {
            $Name = (Import-LocalizedData -BaseDirectory $ModuleBase -FileName Build -ErrorAction SilentlyContinue).Path -replace "\.psd1$"
        }
    } else {
        $Name = (Split-Path $Name -Leaf) -replace "\.psd1$"
    }

    $Manifest = Join-Path $ModuleBase "$Name.psd1"
    if (!(Test-Path $Manifest)) {
        Pop-Location -StackName ResolveModuleManifest
        throw "Can't find module manifest $Manifest"
    }

    Pop-Location -StackName ResolveModuleManifest
    $Manifest
}
#EndRegion '.\Private\ResolveModuleManifest.ps1' 51
#Region '.\Private\ResolveModuleSource.ps1' 0
function ResolveModuleSource {
    <#
        .Synopsis
            Resolve the module source path to the root of the module source code.
    #>

    [OutputType([string])]
    param(
        # The path to the module folder, manifest or build.psd1
        [Parameter(Position = 0, ValueFromPipelineByPropertyName)]
        [ValidateScript( {
                if (Test-Path $_) {
                    $true
                } else {
                    throw "Path must point to an existing file or folder: $_"
                }
            })]
        [Alias("ModuleManifest")]
        [string]$SourcePath = $(Get-Location -PSProvider FileSystem)
    )

    $ModuleBase = Split-Path $SourcePath -Parent
    # Do not use GetFileNameWithoutExtension, because some module names have dots in them
    $ModuleName = (Split-Path $SourcePath -Leaf) -replace "\.psd1$"

    # If $SourcePath points to a build file, switch to the manifest
    if (Test-Path $SourcePath -PathType Leaf) {
        if ($ModuleName -eq "Build") {
            $SourcePath = $ModuleBase
        }
    }

    # If $SourcePath is a folder, check for a matching module manifest or build.psd1
    if (Test-Path $SourcePath -PathType Container) {
        # If we're in a "well known" source folder, look higher for a name
        if ($ModuleName -in "Source", "src") {
            $ModuleName = Split-Path $ModuleBase -Leaf
        }
        if ( (Test-Path (Join-Path $SourcePath build.psd1)) -or (Test-Path (Join-Path $SourcePath "$ModuleName.psd1")) ) {
            $ModuleBase = $SourcePath
        } else {
            throw "No module found in $SourcePath. Try passing the full path to the module manifest file."
        }
    }

    Convert-Path $ModuleBase
}
#EndRegion '.\Private\ResolveModuleSource.ps1' 46
#Region '.\Private\ResolveOutputFolder.ps1' 0
function ResolveOutputFolder {
    [CmdletBinding()]
    param(
        # Where to build the module.
        # Defaults to an \output folder, adjacent to the "SourcePath" folder
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string]$OutputDirectory,

        # If set (true) adds a folder named after the version number to the OutputDirectory
        [Parameter(ValueFromPipelineByPropertyName)]
        [switch]$VersionedOutputDirectory,

        # specifies the module version for use in the output path if -VersionedOutputDirectory is true
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("ModuleVersion")]
        [string]$Version
    )
    process {
        Write-Verbose "Resolve OutputDirectory path: $OutputDirectory"
        # Make sure the OutputDirectory exists (assumes we're in the module source directory)
        $OutputDirectory = New-Item $OutputDirectory -ItemType Directory -Force | Convert-Path
        if ($VersionedOutputDirectory -and $OutputDirectory.TrimEnd("/\") -notmatch "\d+\.\d+\.\d+$") {
            $OutputDirectory = New-Item (Join-Path $OutputDirectory $Version) -ItemType Directory -Force | Convert-Path
            Write-Verbose "Added ModuleVersion to OutputDirectory path: $OutputDirectory"
        }
        $OutputDirectory
    }
}
#EndRegion '.\Private\ResolveOutputFolder.ps1' 28
#Region '.\Private\SetModuleContent.ps1' 0
function SetModuleContent {
    <#
        .SYNOPSIS
            A wrapper for Set-Content that handles arrays of file paths
        .DESCRIPTION
            The implementation here is strongly dependent on Build-Module doing the right thing
            Build-Module can optionally pass a PREFIX or SUFFIX, but otherwise only passes files
 
            Because of that, SetModuleContent doesn't test for that
 
            The goal here is to pretend this is a pipeline, for the sake of memory and file IO
    #>

    [CmdletBinding()]
    param(
        # Where to write the joined output
        [Parameter(Position=0, Mandatory)]
        [string]$OutputPath,

        # Input files, the scripts that will be copied to the output path
        # The FIRST and LAST items can be text content instead of file paths.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias("PSPath", "FullName")]
        [AllowEmptyCollection()]
        [string[]]$SourceFile,

        # The working directory (allows relative paths for other values)
        [string]$WorkingDirectory = $pwd,

        # The encoding defaults to UTF8 (or UTF8NoBom on Core)
        [Parameter(DontShow)]
        [string]$Encoding = $(if($IsCoreCLR) { "UTF8NoBom" } else { "UTF8" })
    )
    begin {
        Push-Location $WorkingDirectory -StackName SetModuleContent
        $ContentStarted = $false # There has been no content yet

        # Create a proxy command style scriptblock for Set-Content to keep the file handle open
        $SetContentCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Set-Content', [System.Management.Automation.CommandTypes]::Cmdlet)
        $SetContent = {& $SetContentCmd -Path $OutputPath -Encoding $Encoding }.GetSteppablePipeline($myInvocation.CommandOrigin)
        $SetContent.Begin($true)
    }
    process  {
        foreach($file in $SourceFile) {
            if($SourceName = Resolve-Path $file -Relative -ErrorAction SilentlyContinue) {
                Write-Verbose "Adding $SourceName"
                $SetContent.Process("#Region '$SourceName' 0")
                Get-Content $SourceName -OutVariable source | ForEach-Object { $SetContent.Process($_) }
                $SetContent.Process("#EndRegion '$SourceName' $($Source.Count)")
            } else {
                if(!$ContentStarted) {
                    $SetContent.Process("#Region 'PREFIX' 0")
                    $SetContent.Process($file)
                    $SetContent.Process("#EndRegion 'PREFIX'")
                    $ContentStarted = $true
                } else {
                    $SetContent.Process("#Region 'SUFFIX' 0")
                    $SetContent.Process($file)
                    $SetContent.Process("#EndRegion 'SUFFIX'")
                }
            }
        }
    }
    end {
        $SetContent.End()
        Pop-Location -StackName SetModuleContent
    }
}
#EndRegion '.\Private\SetModuleContent.ps1' 67
#Region '.\Public\Build-Module.ps1' 0
if (!(Get-Verb Build) -and $MyInvocation.Line -notmatch "DisableNameChecking") {
    Write-Warning "The verb 'Build' was approved recently, but PowerShell $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) doesn't know. You will be warned about Build-Module."
}

function Build-Module {
    <#
        .Synopsis
            Compile a module from ps1 files to a single psm1
        .Description
            Compiles modules from source according to conventions:
            1. A single ModuleName.psd1 manifest file with metadata
            2. Source subfolders in the same directory as the manifest:
               Enum, Classes, Private, Public contain ps1 files
            3. Optionally, a build.psd1 file containing settings for this function
 
            The optimization process:
            1. The OutputDirectory is created
            2. All psd1/psm1/ps1xml files (except build.psd1) in the root will be copied to the output
            3. If specified, $CopyDirectories will be copied to the output
            4. The ModuleName.psm1 will be generated (overwritten completely) by concatenating all .ps1 files in the $SourceDirectories subdirectories
            5. The ModuleVersion and ExportedFunctions in the ModuleName.psd1 may be updated (depending on parameters)
 
        .Example
            Build-Module -Suffix "Export-ModuleMember -Function *-* -Variable PreferenceVariable"
 
            This example shows how to build a simple module from it's manifest, adding an Export-ModuleMember as a Suffix
 
        .Example
            Build-Module -Prefix "using namespace System.Management.Automation"
 
            This example shows how to build a simple module from it's manifest, adding a using statement at the top as a prefix
 
        .Example
            $gitVersion = gitversion | ConvertFrom-Json | Select -Expand InformationalVersion
            Build-Module -SemVer $gitVersion
 
            This example shows how to use a semantic version from gitversion to version your build.
            Note, this is how we version ModuleBuilder, so if you want to see it in action, check out our azure-pipelines.yml
            https://github.com/PoshCode/ModuleBuilder/blob/master/azure-pipelines.yml
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Build is approved now")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")]
    [CmdletBinding(DefaultParameterSetName="SemanticVersion")]
    param(
        # The path to the module folder, manifest or build.psd1
        [Parameter(Position = 0, ValueFromPipelineByPropertyName)]
        [ValidateScript({
            if (Test-Path $_) {
                $true
            } else {
                throw "Source must point to a valid module"
            }
        })]
        [Alias("ModuleManifest", "Path")]
        [string]$SourcePath = $(Get-Location -PSProvider FileSystem),

        # Where to build the module.
        # Defaults to an ..\output folder (adjacent to the "SourcePath" folder)
        [Alias("Destination")]
        [string]$OutputDirectory = "..\Output",

        # If set (true) adds a folder named after the version number to the OutputDirectory
        [switch]$VersionedOutputDirectory,

        # Semantic version, like 1.0.3-beta01+sha.22c35ffff166f34addc49a3b80e622b543199cc5
        # If the SemVer has metadata (after a +), then the full Semver will be added to the ReleaseNotes
        [Parameter(ParameterSetName="SemanticVersion")]
        [string]$SemVer,

        # The module version (must be a valid System.Version such as PowerShell supports for modules)
        [Alias("ModuleVersion")]
        [Parameter(ParameterSetName="ModuleVersion", Mandatory)]
        [version]$Version = $(if($V = $SemVer.Split("+")[0].Split("-")[0]){$V}),

        # Setting pre-release forces the release to be a pre-release.
        # Must be valid pre-release tag like PowerShellGet supports
        [Parameter(ParameterSetName="ModuleVersion")]
        [string]$Prerelease = $($SemVer.Split("+")[0].Split("-")[1]),

        # Build metadata (like the commit sha or the date).
        # If a value is provided here, then the full Semantic version will be inserted to the release notes:
        # Like: ModuleName v(Version(-Prerelease?)+BuildMetadata)
        [Parameter(ParameterSetName="ModuleVersion")]
        [string]$BuildMetadata = $($SemVer.Split("+")[1]),

        # Folders which should be copied intact to the module output
        # Can be relative to the module folder
        [AllowEmptyCollection()]
        [string[]]$CopyDirectories = @(),

        # Folders which contain source .ps1 scripts to be concatenated into the module
        # Defaults to Enum, Classes, Private, Public
        [string[]]$SourceDirectories = @(
            "Enum", "Classes", "Private", "Public"
        ),

        # A Filter (relative to the module folder) for public functions
        # If non-empty, ExportedFunctions will be set with the file BaseNames of matching files
        # Defaults to Public\*.ps1
        [AllowEmptyString()]
        [string[]]$PublicFilter = "Public\*.ps1",

        # File encoding for output RootModule (defaults to UTF8)
        # Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5)
        [ValidateSet("UTF8","UTF7","ASCII","Unicode","UTF32")]
        [string]$Encoding = "UTF8",

        # The prefix is either the path to a file (relative to the module folder) or text to put at the top of the file.
        # If the value of prefix resolves to a file, that file will be read in, otherwise, the value will be used.
        # The default is nothing. See examples for more details.
        [string]$Prefix,

        # The Suffix is either the path to a file (relative to the module folder) or text to put at the bottom of the file.
        # If the value of Suffix resolves to a file, that file will be read in, otherwise, the value will be used.
        # The default is nothing. See examples for more details.
        [Alias("ExportModuleMember","Postfix")]
        [string]$Suffix,

        # Controls whether or not there is a build or cleanup performed
        [ValidateSet("Clean", "Build", "CleanBuild")]
        [string]$Target = "CleanBuild",

        # Output the ModuleInfo of the "built" module
        [switch]$Passthru
    )

    begin {
        if ($Encoding -ne "UTF8") {
            Write-Warning "We strongly recommend you build your script modules with UTF8 encoding for maximum cross-platform compatibility."
        }
    }
    process {
        try {
            # BEFORE we InitializeBuild we need to "fix" the version
            if($PSCmdlet.ParameterSetName -ne "SemanticVersion") {
                Write-Verbose "Calculate the Semantic Version from the $Version - $Prerelease + $BuildMetadata"
                $SemVer = "$Version"
                if($Prerelease) {
                    $SemVer = "$Version-$Prerelease"
                }
                if($BuildMetadata) {
                    $SemVer = "$SemVer+$BuildMetadata"
                }
            }

            # Push into the module source (it may be a subfolder)
            $ModuleInfo = InitializeBuild $SourcePath
            Write-Progress "Building $($ModuleInfo.Name)" -Status "Use -Verbose for more information"
            Write-Verbose  "Building $($ModuleInfo.Name)"

            # Output file names
            $OutputDirectory = $ModuleInfo | ResolveOutputFolder
            $RootModule = Join-Path $OutputDirectory "$($ModuleInfo.Name).psm1"
            $OutputManifest = Join-Path $OutputDirectory "$($ModuleInfo.Name).psd1"
            Write-Verbose  "Output to: $OutputDirectory"

            if ($Target -match "Clean") {
                Write-Verbose "Cleaning $OutputDirectory"
                if (Test-Path $OutputDirectory -PathType Leaf) {
                    throw "Unable to build. There is a file in the way at $OutputDirectory"
                }
                if (Test-Path $OutputDirectory -PathType Container) {
                    if (Get-ChildItem $OutputDirectory\*) {
                        Remove-Item $OutputDirectory\* -Recurse -Force
                    }
                }
                if ($Target -notmatch "Build") {
                    return # No build, just cleaning
                }
            } else {
                # If we're not cleaning, skip the build if it's up to date already
                Write-Verbose "Target $Target"
                $NewestBuild = (Get-Item $RootModule -ErrorAction SilentlyContinue).LastWriteTime
                $IsNew = Get-ChildItem $ModuleInfo.ModuleBase -Recurse |
                    Where-Object LastWriteTime -gt $NewestBuild |
                    Select-Object -First 1 -ExpandProperty LastWriteTime
                if ($null -eq $IsNew) {
                    return # Skip the build
                }
            }
            $null = New-Item -ItemType Directory -Path $OutputDirectory -Force

            # Note that this requires that the module manifest be in the "root" of the source directories
            Set-Location $ModuleInfo.ModuleBase

            Write-Verbose "Copy files to $OutputDirectory"
            # Copy the files and folders which won't be processed
            Copy-Item *.psm1, *.psd1, *.ps1xml -Exclude "build.psd1" -Destination $OutputDirectory -Force
            if ($ModuleInfo.CopyDirectories) {
                Write-Verbose "Copy Entire Directories: $($ModuleInfo.CopyDirectories)"
                Copy-Item -Path $ModuleInfo.CopyDirectories -Recurse -Destination $OutputDirectory -Force
            }

            Write-Verbose "Combine scripts to $RootModule"

            # SilentlyContinue because there don't *HAVE* to be functions at all
            $AllScripts = Get-ChildItem -Path $SourceDirectories.ForEach{ Join-Path $ModuleInfo.ModuleBase $_ } -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue

            SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{$_} -Output $RootModule -Encoding "$($ModuleInfo.Encoding)"

            # If there is a PublicFilter, update ExportedFunctions
            if ($ModuleInfo.PublicFilter) {
                # SilentlyContinue because there don't *HAVE* to be public functions
                if ($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName) {
                    Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $PublicFunctions
                }
            }

            try {
                if ($Version) {
                    Write-Verbose "Update Manifest at $OutputManifest with version: $Version"
                    Update-Metadata -Path $OutputManifest -PropertyName ModuleVersion -Value $Version
                }
            } catch {
                Write-Warning "Failed to update version to $Version. $_"
            }

            if ($null -ne (Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -ErrorAction SilentlyContinue)) {
                if ($Prerelease) {
                    Write-Verbose "Update Manifest at $OutputManifest with Prerelease: $Prerelease"
                    Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value $Prerelease
                } else {
                    Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.Prerelease -Value ""
                }
            } elseif($Prerelease) {
                Write-Warning ("Cannot set Prerelease in module manifest. Add an empty Prerelease to your module manifest, like:`n" +
                               ' PrivateData = @{ PSData = @{ Prerelease = "" } }')
            }

            if ($BuildMetadata) {
                Write-Verbose "Update Manifest at $OutputManifest with metadata: $BuildMetadata from $SemVer"
                $RelNote = Get-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -ErrorAction SilentlyContinue
                if ($null -ne $RelNote) {
                    $Line = "$($ModuleInfo.Name) v$($SemVer)"
                    if ([string]::IsNullOrWhiteSpace($RelNote)) {
                        Write-Verbose "New ReleaseNotes:`n$Line"
                        Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $Line
                    } elseif ($RelNote -match "^\s*\n") {
                        # Leading whitespace includes newlines
                        Write-Verbose "Existing ReleaseNotes:$RelNote"
                        $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`$_"
                        Write-Verbose "New ReleaseNotes:$RelNote"
                        Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote
                    } else {
                        Write-Verbose "Existing ReleaseNotes:`n$RelNote"
                        $RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`n`$_"
                        Write-Verbose "New ReleaseNotes:`n$RelNote"
                        Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote
                    }
                }
            }

            # This is mostly for testing ...
            if ($Passthru) {
                Get-Module $OutputManifest -ListAvailable
            }
        } finally {
            Pop-Location -StackName Build-Module -ErrorAction SilentlyContinue
        }
    }
}
#EndRegion '.\Public\Build-Module.ps1' 261
#Region '.\Public\Convert-CodeCoverage.ps1' 0
function Convert-CodeCoverage {
    <#
        .SYNOPSIS
            Convert the file name and line numbers from Pester code coverage of "optimized" modules to the source
        .EXAMPLE
            Invoke-Pester .\Tests -CodeCoverage (Get-ChildItem .\Output -Filter *.psm1).FullName -PassThru |
                Convert-CodeCoverage -SourceRoot .\Source -Relative
 
            Runs pester tests from a "Tests" subfolder against an optimized module in the "Output" folder,
            piping the results through Convert-CodeCoverage to render the code coverage misses with the source paths.
    #>

    param(
        # The root of the source folder (for resolving source code paths)
        [Parameter(Mandatory)]
        [string]$SourceRoot,

        # The output of `Invoke-Pester -Pasthru`
        # Note: Pester doesn't apply a custom type name
        [Parameter(ValueFromPipeline)]
        [PSObject]$InputObject,

        # Output paths as short paths, relative to the SourceRoot
        [switch]$Relative
    )
    process {
        Push-Location $SourceRoot
        try {
            $InputObject.CodeCoverage.MissedCommands | Convert-LineNumber -Passthru |
                Select-Object SourceFile, @{Name="Line"; Expr={$_.SourceLineNumber}}, Command
        } finally {
            Pop-Location
        }
    }
}
#EndRegion '.\Public\Convert-CodeCoverage.ps1' 34
#Region '.\Public\Convert-LineNumber.ps1' 0
function Convert-LineNumber {
    <#
        .SYNOPSIS
            Convert the line number in a built module to a file and line number in source
        .EXAMPLE
            Convert-LineNumber -SourceFile ~\ErrorMaker.psm1 -SourceLineNumber 27
        .EXAMPLE
            Convert-LineNumber -PositionMessage "At C:\Users\Joel\OneDrive\Documents\PowerShell\Modules\ErrorMaker\ErrorMaker.psm1:27 char:4"
    #>

    [CmdletBinding(DefaultParameterSetName="FromString")]
    param(
        # A position message as found in PowerShell's error messages, ScriptStackTrace, or InvocationInfo
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName="FromString")]
        [string]$PositionMessage,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=0, ParameterSetName="FromInvocationInfo")]
        [Alias("PSCommandPath", "File", "ScriptName")]
        [string]$SourceFile,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position=1, ParameterSetName="FromInvocationInfo")]
        [Alias("LineNumber", "Line", "ScriptLineNumber")]
        [int]$SourceLineNumber,

        [Parameter(ValueFromPipeline, DontShow, ParameterSetName="FromInvocationInfo")]
        [psobject]$InputObject,

        [Parameter(ParameterSetName="FromInvocationInfo")]
        [switch]$Passthru,

        # Output paths as short paths, relative to the SourceRoot
        [switch]$Relative
    )
    begin {
        $filemap = @{}
    }
    process {
        if($PSCmdlet.ParameterSetName -eq "FromString") {
            $Invocation = ParseLineNumber $PositionMessage
            $SourceFile = $Invocation.SourceFile
            $SourceLineNumber = $Invocation.SourceLineNumber
        }
        $PSScriptRoot = Split-Path $SourceFile

        if(!(Test-Path $SourceFile)) {
            throw "'$SourceFile' does not exist"
        }

        Push-Location $PSScriptRoot
        try {
            if (!$filemap.ContainsKey($SourceFile)) {
                # Note: the new pattern is #Region but the old one was # BEGIN
                $matches = Select-String '^(?:#Region|# BEGIN) (?<SourceFile>.*) (?<LineNumber>\d+)?$' -Path $SourceFile
                $filemap[$SourceFile] = @($matches.ForEach{
                        [PSCustomObject]@{
                            PSTypeName = "BuildSourceMapping"
                            SourceFile = $_.Matches[0].Groups["SourceFile"].Value.Trim("'")
                            StartLineNumber = $_.LineNumber
                        }
                    })
            }

            $hit = $filemap[$SourceFile]

            # These are all negative, because BinarySearch returns the match *after* the line we're searching for
            # We need the match *before* the line we're searching for
            # And we need it as a zero-based index:
            $index = -2 - [Array]::BinarySearch($hit.StartLineNumber, $SourceLineNumber)
            $Source = $hit[$index]

            if($Passthru) {
                $InputObject |
                    Add-Member -MemberType NoteProperty -Name SourceFile -Value $Source.SourceFile -PassThru -Force |
                    Add-Member -MemberType NoteProperty -Name SourceLineNumber -Value ($SourceLineNumber - $Source.StartLineNumber) -PassThru -Force
            } else {
                [PSCustomObject]@{
                    PSTypeName = "SourceLocation"
                    SourceFile = $Source.SourceFile
                    SourceLineNumber = $SourceLineNumber - $Source.StartLineNumber
                }
            }
        } finally {
            Pop-Location
        }
    }
}
#EndRegion '.\Public\Convert-LineNumber.ps1' 85

# SIG # Begin signature block
# MIIXzgYJKoZIhvcNAQcCoIIXvzCCF7sCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUWjEulhZffjgOjGT9zfqW6kAk
# dvmgghMBMIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B
# AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG
# A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh
# d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg
# Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV
# UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu
# dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q
# WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC
# i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4
# ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3
# +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI
# fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd
# BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG
# CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro
# YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV
# HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y
# MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf
# plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y
# 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq
# IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3
# DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh
# dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD
# QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE
# BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT
# eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow
# mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0
# jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu
# ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh
# d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz
# C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB
# o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO
# BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw
# Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90
# cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx
# oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy
# bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV
# HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa
# 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH
# bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73
# BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR
# EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW
# yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu
# e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw
# ggUwMIIEGKADAgECAhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9v
# dCBDQTAeFw0xMzEwMjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYT
# AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy
# dC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNp
# Z25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4R
# r2d3B9MLMUkZz9D7RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrw
# nIal2CWsDnkoOn7p0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnC
# wlLyFGeKiUXULaGj6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8
# y5Kh5TsxHM/q8grkV7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM
# 0SAlI+sIZD5SlsHyDxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6f
# pjOp/RnfJZPRAgMBAAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
# DwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGsw
# JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcw
# AoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElE
# Um9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNl
# cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDov
# L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBP
# BgNVHSAESDBGMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93
# d3cuZGlnaWNlcnQuY29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoK
# o6XqcQPAYPkt9mV1DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8w
# DQYJKoZIhvcNAQELBQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+
# C2D9wz0PxK+L/e8q3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119E
# efM2FAaK95xGTlz/kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR
# 4pwUR6F6aGivm6dcIFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4v
# cn4c10lFluhZHen6dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwH
# gfqL2vmCSfdibqFT+hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUwMIIEGKADAgEC
# AhALDZkX0sdOvwJhwzQTbV+7MA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25p
# bmcgQ0EwHhcNMTgwNzEyMDAwMDAwWhcNMTkwNzE2MTIwMDAwWjBtMQswCQYDVQQG
# EwJVUzERMA8GA1UECBMITmV3IFlvcmsxFzAVBgNVBAcTDldlc3QgSGVucmlldHRh
# MRgwFgYDVQQKEw9Kb2VsIEguIEJlbm5ldHQxGDAWBgNVBAMTD0pvZWwgSC4gQmVu
# bmV0dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJb3Cf3n+/pFJiO
# hQqN5m54FpyIktMRWe5VyF8465BnAtzw3ivMyN+3k8IoXQhMxpCsY1TJbLyydNR2
# QzwEEtGfcTVnlAJdFFlBsgIdK43waaML5EG7tzNJKhHQDiN9bVhLPTXrit80eCTI
# RpOA7435oVG8erDpxhJUK364myUrmSyF9SbUX7uE09CJJgtB7vqetl4G+1j+iFDN
# Xi3bu1BFMWJp+TtICM+Zc5Wb+ZaYAE6V8t5GCyH1nlAI3cPjqVm8y5NoynZTfOhV
# bHiV0QI2K5WrBBboR0q6nd4cy6NJ8u5axi6CdUhnDMH20NN2I0v+2MBkgLAzxPrX
# kjnaEGECAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5LfZl
# dQ5YMB0GA1UdDgQWBBTiwur/NVanABEKwjZDB3g6SZN1mTAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0cDov
# L2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAzoDGG
# L2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3Js
# MEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYw
# JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcw
# AoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3Vy
# ZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL
# BQADggEBADNNHuRAdX0ddONqaUf3H3pwa1K016C02P90xDIyMvw+hiUb4Z/xewnY
# jyplspD0NQB9ca2pnNIy1KwjJryRgq8gl3epSiWTbViVn6VDK2h0JXm54H6hczQ8
# sEshCW53znNVUUUfxGsVM9kMcwITHYftciW0J+SsGcfuuAIuF1g47KQXKWOMcUQl
# yrP5t0ywotTVcg/1HWAPFE0V0sFy+Or4n81+BWXOLaCXIeeryLYncAVUBT1DI6lk
# peRUj/99kkn+hz1q4hHTtfNpMTOApP64EEFGKICKkJdvhs1PjtGa+QdAkhcInTxk
# t/hIJPUb1nO4CsKp1gaVsRkkbcStJ2kxggQ3MIIEMwIBATCBhjByMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBT
# aWduaW5nIENBAhALDZkX0sdOvwJhwzQTbV+7MAkGBSsOAwIaBQCgeDAYBgorBgEE
# AYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwG
# CisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBSDks53
# qcZa17k82S9M6PAi6vs2gzANBgkqhkiG9w0BAQEFAASCAQB+wxUEBguQ3ir9mxGM
# Tn7QzffcQLd5by5pFsifqEXPac0wEqJJtz+TTsHdLiFJ5dRflJehvkVVhJFoAz7j
# OtTdhUZtOegwwA6k5oCAjr2YaepWUqNvZr/FKArTuMKZ27qYSnNIKSQRGXdlyh/f
# hqaF7bUkKeQ+rlAU/xaMB/THJYkD3gdgRNDxOkUKbbDChdLE9FInu71AgO6MqW+e
# 1a/VTCqSQjWOHfTQLHM36BuBNJNIYxhVrozTaWGrRI5TA3HqKMZRvcI2bpZJmznf
# pQC9zEIdrZTkcr8hXKyIHAavRUY1VOhMW6ppfgz+EZTbKJd0sxsspN6ylGd7qMKI
# 2A9goYICCzCCAgcGCSqGSIb3DQEJBjGCAfgwggH0AgEBMHIwXjELMAkGA1UEBhMC
# VVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTAwLgYDVQQDEydTeW1h
# bnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIENBIC0gRzICEA7P9DjI/r81bgTY
# apgbGlAwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJ
# KoZIhvcNAQkFMQ8XDTE4MTEwODA2MDIyN1owIwYJKoZIhvcNAQkEMRYEFE8t7Vy1
# k46faUcwk/F0aQD7XTMDMA0GCSqGSIb3DQEBAQUABIIBAH/reCZsOHUPuhZHLP74
# 0oQTnzJvv7T022ZcGWn72Dq74eujGI0pfvaTWARiUE/KXKcLUEDlxTToojSNcE2F
# yWVqrsOCpJoy/xckWJ0inaWGzAw/09SuxlzLVpRGNCOjYsMGlEvbwLcVGKF/f+u0
# IIvJldziaLvo6KdyEmqOlpmAqFz833r7Bvl/08xPcUuM7DyRAjIBu5P6r72Vq0ln
# xNjEZ13nQyewSFBCosu/SF3trby+P4SdUXcMaXfjYrai8JJTAiNXU6XPG2/0JgYw
# eW59Q/OM6ijL99rfGtRDiRFVPszvxKXjtLp2ROShY3388CKu9pnS32sZtoElVSZ3
# SL0=
# SIG # End signature block