PSModuleBuild.psm1
Function Invoke-PSModuleBuild { <# .SYNOPSIS Easily build PowerShell modules for a set of functions contained in individual PS1 files .DESCRIPTION Creating a PowerShell module can be hard, and maintaining it can be even harder. PSModuleBuild has been designed to make both tasks easier. In short, you put all of your advanced functions into individual .ps1 files and then invoke PSModuleBuild and let it collect all the functions into a PowerShell module file (.psm1) and create the PowerShell module manifest file (.psd1). Files can be excluded from processing by putting these keywords in their name: exclude tests psake\.ps1 ^build\.ps1$ \.psdeploy\. Public and private functions are also support by creating the proper folder structure: \Source \Public \Private If you have any scripts or cmdlets that need to be run at Import-Module time, you can put them in an Include.txt file and PSModuleBuild will read this file first and put it in the module file first. This is not strictly needed as PSModuleBuild will read in all .ps1 files and put them in but if you'd like to make sure these commands are run at the beginning of the file you can. .PARAMETER Path The path where you module folders and PS1 files containing your functions is located. .PARAMETER TargetPath The path where you want the module and manifest files to be located. If the folder does not exist Invoke-PSModuleBuild will create it, if it is not designated it will use the Path location. Multiple folder paths can be designated as well if you want to deploy to multiple locations. .PARAMETER ModuleName What you want to call your module. By default the module will be named after the folder you point to in Path. .PARAMETER Passthru Will produce an object with information about the newly created module .PARAMETER ReleaseNotes Any release notes you want to include in the module manifest. If a manifest file already exists Invoke-PSModuleBuild will read the release notes from it and join the new release notes together. .INPUTS None .OUTPUTS [PSCustomObject] .EXAMPLE Invoke-PSModuleBuild -Path c:\Test-Module Module will be named Test-Module (.psm1 and .psd1) and will include all functions in that path. .EXAMPLE Invoke-PSModuleBuild -Path c:\Test-Module -ModuleName Make-GreatStuff -Passthru Module will be named Make-GreatStuff. Returned object will be: Name : Make-GreatStuff Path : c:\Test-Module ManifestPath : c:\Test-Module\Test-Module.psd1 ModulePath : c:\Test-Module\Test-Module.psm1 PublicFunctions : {Test1, Test2} PrivateFunctions: {Test3} .NOTES Author: Martin Pugh Twitter: @thesurlyadm1n Spiceworks: Martin9700 Blog: www.thesurlyadmin.com Changelog: 1.0 Initial Release 1.0.9 Moved from RegEx to AST for function parsing 1.0.10 Updated comment based help. Added Passthru parameter 1.0.11 Updated comment based help. Exclude psake.ps1, build.ps1 and .psdeploy. from function import. Added BuildVersion 1.0.12 Removed BuildVersion. Added dynamic parameters from New-ModuleManifest. 1.0.13 Removed a debugging line. 1.0.14 Rename to Invoke-PSModuleBuild and create module named PSModuleBuild. Added ReleaseNotes support (New and Update-ModuleManifest treat ReleaseNotes differently) 1.0.15 Updated comment based help 1.1 Added multiple target paths .LINK https://github.com/martin9700/PSModuleBuild #> [CmdletBinding()] Param ( [ValidateScript({ Test-Path $_ })] [string]$Path, [string[]]$TargetPath, [string]$ModuleName, [switch]$Passthru, [string[]]$ReleaseNotes ) DynamicParam { # Create the dictionary that this scriptblock will return: $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $CommonParams = [System.Management.Automation.PSCmdlet]::CommonParameters $CommonParams += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters # Get dynamic params that real Cmdlet would have: $Parameters = Get-Command -Name New-ModuleManifest | Select-Object -ExpandProperty Parameters ForEach ($Parameter in $Parameters.GetEnumerator()) { If ($CommonParams -notcontains $Parameter.Key) { $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( $Parameter.Key, $Parameter.Value.ParameterType, $Parameter.Value.Attributes ) #Added in check to not add Name or NotificationEmail parameters because they are defined in static parameters If (-not $DynParamDictionary.ContainsKey($Parameter.Key) -and $Parameter.Key -notmatch "Path|Passthru|ReleaseNotes") { $DynParamDictionary.Add($Parameter.Key, $DynamicParameter) } } } # Return the dynamic parameters $DynParamDictionary } PROCESS { Write-Verbose "$(Get-Date): Invoke-PSModuleBuild started" If (-not $Path) { $Path = $PSScriptRoot } If ($TargetPath) { ForEach ($TP in $TargetPath) { If (-not (Test-Path $TP)) { New-Item -Path $TP -ItemType Directory } } } Else { $TargetPath = $Path } If (-not $ModuleName) { $ModuleName = Get-ItemProperty -Path $Path | Select -ExpandProperty BaseName } $Module = New-Object -TypeName System.Collections.ArrayList $FunctionNames = New-Object -TypeName System.Collections.ArrayList $FunctionPredicate = { ($args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]) } $HighVersion = [version]"2.0" Write-Verbose "$(Get-Date): Searching for ps1 files and include.txt for module" #Retrieve Include.txt file(s) $Files = Get-ChildItem $Path\Include.txt -Recurse | Sort FullName ForEach ($File in $Files) { $Raw = Get-Content $File $null = $Module.Add($Raw) } #Retrieve ps1 files $Files = Get-ChildItem $Path\*.ps1 -File -Recurse | Where FullName -NotMatch "Exclude|Tests|psake\.ps1|^build\.ps1|\.psdeploy\." | Sort FullName ForEach ($File in $Files) { $Raw = Get-Content $File -Raw $Private = $false If ($File.DirectoryName -like "*Private*") { $Private = $true } $null = $Module.Add($Raw) #Parse out the function names #Thanks Zachary Loeber $ParseError = $null $Tokens = $null $AST = [System.Management.Automation.Language.Parser]::ParseInput($Raw, [ref]$Tokens, [ref]$ParseError) If ($ParseError) { Write-Error "Unable to parse $($File.FullName) because ""$ParseError""" -ErrorAction Stop } ForEach ($Name in ($AST.FindAll($FunctionPredicate, $true) | Select -ExpandProperty Name)) { If ($FunctionNames.Name -contains $Name) { Write-Error "Your module has duplicate function names: $Name. Duplicate found in $($File.FullName)" -ErrorAction Stop } Else { $null = $FunctionNames.Add([PSCustomObject]@{ Name = $Name Private = $Private }) } } If ($AST.ScriptRequirements.RequiredPSVersion -gt $HighVersion) { $HighVersion = $AST.ScriptRequirements.RequiredPSVersion } } #Create the manifest file Write-Verbose "$(Get-Date): Creating/Updating module manifest and module file" $Manifest = @{} ForEach ($Key in ($PSBoundParameters.GetEnumerator() | Where { $_.Key -NotMatch "Path|Passthru|ModuleName" -and $CommonParams -notcontains $_.Key })) { $Manifest.Add($Key.Key,$Key.Value) } ForEach ($TP in $TargetPath) { $ManifestPath = Join-Path -Path $TP -ChildPath "$ModuleName.psd1" If (Test-Path $ManifestPath) { $OldManifest = Import-LocalizedData -BaseDirectory (Split-Path $ManifestPath) -FileName (Split-Path $ManifestPath -Leaf) If ([version]$OldManifest.PowerShellVersion -gt $HighVersion) { $HighVersion = [version]$OldManifest.PowerShellVersion } If ($OldManifest.PrivateData.PSData.ReleaseNotes) { $Manifest.ReleaseNotes = $ReleaseNotes + $OldManifest.PrivateData.PSData.ReleaseNotes } If ($Manifest.PowerShellVersion -gt $HighVersion) { $HighVersion = $Manifest.PowerShellVersion } $Manifest.Path = $ManifestPath $Manifest.PowerShellVersion = $HighVersion $Manifest.FunctionsToExport = $FunctionNames | Where Private -eq $false | Select -ExpandProperty Name If ($BuildVersion) { $Manifest.Add("ModuleVersion",$BuildVersion) } Update-ModuleManifest @Manifest } Else { $Manifest.RootModule = $ModuleName $Manifest.Path = $ManifestPath $Manifest.PowerShellVersion = "$($HighVersion.Major).$($HighVersion.Minor)" $Manifest.FunctionsToExport = $FunctionNames | Where Private -eq $false | Select -ExpandProperty Name If ($BuildVersion) { $Manifest.Add("ModuleVersion",$BuildVersion) } If ($ReleaseNotes) { $Manifest.ReleaseNotes = $ReleaseNotes | Out-String } New-ModuleManifest @Manifest } } #Save the Module file ForEach ($TP in $TargetPath) { $ModulePath = Join-Path -Path $TP -ChildPath "$ModuleName.psm1" $Module | Out-File $ModulePath -Encoding ascii Write-Verbose "Module created at: $TP as $ModuleName" -Verbose } #Passthru If ($Passthru) { [PSCustomObject]@{ Name = $ModuleName SourcePath = $Path TargetPath = $TargetPath ManifestPath = $ManifestPath ModulePath = $ModulePath RequiredVersion = $Manifest.PowerShellVersion PublicFunctions = @($FunctionNames | Where Private -eq $false | Select -ExpandProperty Name) PrivateFunctions = @($FunctionNames | Where Private -eq $true | Select -ExpandProperty Name) ReleaseNotes = $Manifest.ReleaseNotes } } Write-Verbose "$(Get-Date): Invoke-PSModuleBuild completed." } } |