PSBuild.psm1
#region Private Functions function priv_Export-Artifact { [CmdletBinding()] [OutputType([void])] param ( #SourcePath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Default')] [ValidateScript( { if (-not (test-path $_)) { throw "$_ does not exist" } else { $true } })] [string]$SourcePath, #DestinationPath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Default')] [string]$DestinationPath, #ModuleVersion [Parameter(Mandatory = $false, ParameterSetName = 'NoRemoting_Default')] [string]$Version, #Type [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Default')] [ValidateSet('Script', 'Module', 'ScriptWithConfig')] [string]$Type ) Process { switch ($Type) { 'Script' { #Check if Source and Destination are the same $SourceFolder = Split-Path -Path $SourcePath -Parent -ErrorAction Stop if ($SourceFolder -eq $DestinationPath) { #Do nothing } else { if (-not (Test-Path $DestinationPath)) { $null = New-Item -Path $DestinationPath -ItemType Directory -ErrorAction Stop } $null = Copy-Item -Path $SourcePath -Destination $DestinationPath -Force -ErrorAction Stop } break } 'ScriptWithConfig' { #Check if Source and Destination are the same $SourceFolder = Split-Path -Path $SourcePath -Parent -ErrorAction Stop if ($SourceFolder -eq $DestinationPath) { #Do nothing } else { $ScriptConfigFileName = "$(([System.IO.FileInfo]$SourcePath).BaseName).config.json" $ScriptConfigFilePath = Join-Path -Path $SourceFolder -ChildPath $ScriptConfigFileName if (-not (Test-Path $DestinationPath)) { $null = New-Item -Path $DestinationPath -ItemType Directory -ErrorAction Stop } $null = Copy-Item -Path $SourcePath -Destination $DestinationPath -Force -ErrorAction Stop $null = Copy-Item -Path $ScriptConfigFilePath -Destination $DestinationPath -Force -ErrorAction Stop } break } 'Module' { $ModuleFolder = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $SourcePath $ObjectName = $ModuleFolder.BaseName #Calculcate DestinationFolder if ($PSBoundParameters.ContainsKey('Version')) { $ObjectDestinationFolderTemp = Join-Path -Path $DestinationPath -ChildPath $ObjectName -ErrorAction Stop $ObjectDestinationFolder = Join-Path -Path $ObjectDestinationFolderTemp -ChildPath $Version -ErrorAction Stop } else { $ObjectDestinationFolder = Join-Path -Path $DestinationPath -ChildPath $ObjectName -ErrorAction Stop } #Check if Source and Destination are the same $SourceFolder = Split-Path -Path $SourcePath -Parent -ErrorAction Stop if ($SourceFolder -eq $ObjectDestinationFolder) { #Do nothing } else { #Validate DestinationFolder if (Test-Path -Path $ObjectDestinationFolder) { Remove-Item -Path $ObjectDestinationFolder -ErrorAction Stop -Force -Confirm:$false -Recurse } $null = New-Item -Path $ObjectDestinationFolder -ItemType Directory -ErrorAction Stop #Copy Module content $ModuleFilesToExclude = @( 'obj' 'bin' 'CSharpAssemblies' '*.pssproj' '*.pssproj.user' '*.csproj' '*.csproj.user' '*.vspscc' '*.pdb' '*.cs' '*.tests.ps1' 'packages.config' 'Tests' 'System.Management.Automation.dll' ) $null = Copy-Item -Path "$SourcePath\*" -Destination $ObjectDestinationFolder -Recurse -Exclude $ModuleFilesToExclude } break } default { throw "Unknown Type: $Type" } } } } function priv_Publish-PSModule { [CmdletBinding()] [OutputType([void])] param ( #ModuleInfo [Parameter(Mandatory = $true)] [PSModuleInfo]$ModuleInfo, #Credential [Parameter(Mandatory = $false)] [PScredential]$Credential, #Repository [Parameter(Mandatory = $false)] [string]$Repository, #NuGetApiKey [Parameter(Mandatory = $false)] [string]$NuGetApiKey, #Force [Parameter(Mandatory = $false)] [switch]$Force, #PublishDependantModules [Parameter(Mandatory = $false)] [switch]$PublishDependantModules = $true, #VerbosePrefix [Parameter(Mandatory = $false)] [string]$VerbosePrefix, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { #Resolve ModuleRootFolder try { $ModuleLeafFolder = Split-Path -Path $ModuleInfo.ModuleBase -Leaf $ModuleVersion = New-Object System.Version if ([System.Version]::TryParse($ModuleLeafFolder, [ref]$ModuleVersion)) { $ModuleVersionFolder = Split-Path -Path $ModuleInfo.ModuleBase -Parent $ModuleRootFolder = Split-Path -Path $ModuleVersionFolder -Parent } else { $ModuleRootFolder = Split-Path -Path $ModuleInfo.ModuleBase -Parent } } catch { Write-Error "Resolve ModuleRootFolder failed. Details: $_" -ErrorAction 'Stop' } #Publish RequiredModules $ModsToPublish = $ModuleInfo.RequiredModules | Where-Object { $ModuleInfo.PrivateData.PSData.ExternalModuleDependencies -notcontains $_.Name } foreach ($ReqModule in $ModsToPublish) { $ReqModuleFound = $false #Check if Required Module is present in the same folder as the current module try { $ReqModuleInfo = Get-Module -ListAvailable -FullyQualifiedName "$ModuleRootFolder\$($ReqModule.Name)" -Refresh -ErrorAction Stop if ($ReqModuleInfo) { #If multiple versions are available, select latest one. $ReqModuleInfo = $ReqModuleInfo | Sort-Object -Property Version | select -Last 1 if ($ReqModule.Version -and ($ReqModule.Version -le $ReqModuleInfo.Version)) { $ReqModuleFound = $true } else { $ReqModuleFound = $true } } } catch { } #Check if Required Module is present on the machine if (-not $ReqModuleFound) { try { $ReqModuleInfo = Get-Module -Name $ReqModule.Name -ErrorAction Stop if ($ReqModuleInfo) { #If multiple versions are available, select latest one. $ReqModuleInfo = $ReqModuleInfo | Sort-Object -Property Version | select -Last 1 if ($ReqModule.Version -and ($ReqModule.Version -le $ReqModuleInfo.Version)) { $ReqModuleFound = $true } else { $ReqModuleFound = $true } } } catch { } } if ($ReqModuleFound) { $PublishModuleAndDependacies_Params = @{ } + $PSBoundParameters $PublishModuleAndDependacies_Params['ModuleInfo'] = $ReqModuleInfo priv_Publish-PSModule @PublishModuleAndDependacies_Params } else { throw "Unable to find Required Module: $($ReqModule.Name)" } } #Publish Module try { $PublishModule_CommonParams = @{ Repository = $Repository } if ($PSBoundParameters.ContainsKey('Credential')) { $PublishModule_CommonParams.Add('Credential', $Credential) } if ($PSBoundParameters.ContainsKey('Proxy')) { $PublishModule_CommonParams.Add('Proxy', $Proxy) } #Check if module already exist on the Repository try { Remove-Variable -Name ModExist -ErrorAction SilentlyContinue $private:ModExist = Find-Module @PublishModule_CommonParams -Name $ModuleInfo.Name -RequiredVersion $ModuleInfo.Version -ErrorAction Stop } catch { } if ($ModExist) { Write-Verbose "$VerbosePrefix`Module already exist on the PSGetRepo" } else { $PublishModule_Params = @{ Path = $ModuleInfo.ModuleBase } + $PublishModule_CommonParams if ($PSBoundParameters.ContainsKey('NuGetApiKey')) { $PublishModule_Params.Add('NuGetApiKey', $NuGetApiKey) } Write-Verbose "$VerbosePrefix`Publishing new version" Publish-Module @PublishModule_Params -Force -ErrorAction Stop } } catch { Write-Error "Publish Module failed. Details: $_" -ErrorAction 'Stop' } } } function priv_Get-ModuleDefinition { [CmdletBinding()] param ( #ModulePath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Module')] [string]$ModulePath, #DestinationPath [Parameter(Mandatory = $true)] [string[]]$DestinationPath, #ProactiveRequiredModuleLoading [Parameter(Mandatory = $false)] [bool]$ProactiveRequiredModuleLoading = $false ) Process { try { $JobParams = @{ ArgumentList = @(@{ ModulePath = $ModulePath DestinationPath = $DestinationPath ProactiveRequiredModuleLoading = $ProactiveRequiredModuleLoading }) ScriptBlock = { $VerbosePreference = 'SilentlyContinue' $WarningPreference = 'SilentlyContinue' $cfg = $args[0] $hashSet = [System.Collections.Generic.HashSet[string]]::new(([System.Environment]::GetEnvironmentVariable('PsModulePath', [System.EnvironmentVariableTarget]::Process)).Split(";", [System.StringSplitOptions]::RemoveEmptyEntries), [System.StringComparer]::OrdinalIgnoreCase) $changePending = $false if($cfg.DestinationPath) { foreach($dstPath in $cfg.DestinationPath) { if((-not [string]::IsNullOrEmpty($dstPath)) -and [IO.Directory]::Exists($dstPath)) { if($hashSet.Add($dstPath)) { $changePending = $true } } } } if($changePending) { [System.Environment]::SetEnvironmentVariable('PsModulePath', ($hashSet -join ';'), [System.EnvironmentVariableTarget]::Process) } if($cfg.ProactiveRequiredModuleLoading) { function priv_ImportModule { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [hashtable[]]$ModuleInfo ) foreach($modInfo in $ModuleInfo) { $ReqModObj = Get-Module -Name ($modInfo.ModuleName) -ListAvailable -Refresh -ErrorAction Stop -Verbose:$false | Sort-Object -Property Version -Descending | Select-Object -First 1 if(-not ($ReqModObj)) { if(Test-Path -Path $modInfo.Path) { $ReqModObj = Get-Module -Name ($modInfo.Path) -ListAvailable -Refresh -ErrorAction Stop -Verbose:$false | Sort-Object -Property Version -Descending | Select-Object -First 1 } } if(-not ($ReqModObj)) { throw "Failed to load required module '$($modInfo.ModuleName)'. Module was not found." } if($ReqModObj.RequiredModules) { priv_ImportModule -ModuleInfo @(@($ReqModObj.RequiredModules).ForEach{ @{ ModuleName = ($_.Name); ModuleVersion = ($_.Version); Path = ($_.Path) } }) } if(-not (Get-Module -Name $ReqModObj.Name)) { # Write-Verbose -Message "DEBUG: Importing required module '$($modInfo.ModuleName)'" -Verbose $null = Import-Module -FullyQualifiedName ($ReqModObj.Path) -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false } } } [IO.DirectoryInfo]$PrimaryModulePath = ($cfg.ModulePath) $PrimaryModuleDefinitionFile = Get-ChildItem -Path ($PrimaryModulePath.FullName) -Recurse -filter "$($PrimaryModulePath.Name).psd1" -ErrorAction Stop -File $PrimaryModuleObj = Get-Module -ListAvailable -FullyQualifiedName ($PrimaryModuleDefinitionFile.FullName) -Refresh -ErrorAction Stop -Verbose:$false if($PrimaryModuleObj.RequiredModules) { priv_ImportModule -ModuleInfo @(@($PrimaryModuleObj.RequiredModules).ForEach{ @{ ModuleName = ($_.Name); ModuleVersion = ($_.Version); Path = ($_.Path) } }) } } $ModFresh = Import-Module -FullyQualifiedName ($cfg.ModulePath) -PassThru -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false -ErrorVariable er if (-not $ModFresh) { throw "Unable to Import Module: $($args[0]). Details: $er" } $ModFresh.Definition } } $Job = Start-Job @JobParams -Verbose:$false $null = Wait-Job -Job $Job -Verbose:$false Receive-Job -Job $Job -ErrorAction Stop -Verbose:$false # returns the module definition as string } catch { throw "Failed to load Module Definition. Details: $_" } finally { Remove-Job -Job $Job -Force } } } function priv_Get-ScriptDefinition { [CmdletBinding()] param ( #ModulePath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Module')] [string]$ScriptPath ) Process { try { $JobParams = @{ ArgumentList = @(@{ ScriptPath = $ScriptPath }) ScriptBlock = { $VerbosePreference = 'SilentlyContinue' $WarningPreference = 'SilentlyContinue' $cfg = $args[0] (Get-Command -Name $cfg.ScriptPath -ErrorAction Stop -Verbose:$false).ScriptBlock.ToString() } } $Job = Start-Job @JobParams -Verbose:$false $null = Wait-Job -Job $Job -Verbose:$false Receive-Job -Job $Job -ErrorAction Stop -Verbose:$false # returns the module definition as string } catch { throw "Failed to load Script Definition. Details: $_" } finally { Remove-Job -Job $Job -Force } } } <# function priv_Analyse-ItemDependancies { [CmdletBinding()] param ( #ScriptBlock [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_ScriptBlock')] [scriptblock]$ScriptBlock, #ModulePath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Module')] [string]$ModulePath, #ScriptPath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Script')] [string]$ScriptPath, # #GlobalCommandAnalysis # [Parameter(Mandatory = $true)] # [ref]$GlobalCommandAnalysis, #PSGetRepository [Parameter(Mandatory = $false)] [hashtable[]]$PSGetRepository, #CurrentDependancies [Parameter(Mandatory = $false)] [string[]]$CurrentDependancies, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { #Construct JobParams $JobParams = @{ GlobalCommandAnalysisAsJson = [PSBuild.Context]::Current.CommandsToModuleMapping.ToJson() CurrentDependancies = $CurrentDependancies PSGetRepository = $PSGetRepository PSBuildDllPath = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'PSBuildEntities' } | select -ExpandProperty Location PSBuildModulePath = (Get-Module -Name psbuild).Path AstExtensionsModulePath = (Get-Module -Name AstExtensions).Path } if ($PSBoundParameters.ContainsKey('Proxy')) { $JobParams.Add('Proxy', $Proxy) } Switch ($PSCmdlet.ParameterSetName) { 'NoRemoting_ScriptBlock' { $JobParams.Add('ScriptBlock', $ScriptBlock) break } 'NoRemoting_Module' { $JobParams.Add('ModulePath', $ModulePath) break } 'NoRemoting_Script' { $JobParams.Add('ScriptPath', $ScriptPath) break } default { throw "Unknown ParameterSet: $_" } } try { $Job = Start-Job -ScriptBlock { #Wait-Debugger $JobParams = $Using:JobParams Import-Module -FullyQualifiedName $JobParams["PSBuildModulePath"] Import-Module -FullyQualifiedName $JobParams["AstExtensionsModulePath"] Add-Type -Path $JobParams['PSBuildDllPath'] -ErrorAction Stop $GlobalCommandAnalysis = [PSBuild.CommandAnalysisCollection]::FromJson($JobParams['GlobalCommandAnalysisAsJson']) $LocalCommandAnalysis = [PSBuild.CommandAnalysisCollection]::New() #Get Module ScriptBlockAst if ($JobParams.ContainsKey('ModulePath')) { $OldErrorActionPreference = $ErrorActionPreference $OldVerbosePreference = $VerbosePreference $OldWarningPreference = $WarningPreference $ErrorActionPreference = 'Continue' $VerbosePreference = 'SilentlyContinue' $WarningPreference = 'SilentlyContinue' $ModFresh = Import-Module -FullyQualifiedName $JobParams['ModulePath'] -PassThru -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false -ErrorVariable er $ErrorActionPreference = $OldErrorActionPreference $VerbosePreference = $OldVerbosePreference $WarningPreference = $OldWarningPreference if (-not $ModFresh) { throw "Unable to Import Module: $($JobParams['ModulePath']). Details: $er" } $ModuleSb = [scriptblock]::Create($ModFresh.Definition) $ScriptBlockAst = $ModuleSb.Ast } #Get Script ScriptBlockAst elseif ($JobParams.ContainsKey('ScriptPath')) { $ScriptContent = Get-Command -Name $JobParams['ScriptPath'] -ErrorAction Stop -Verbose:$false $ScriptBlockAst = $ScriptContent.ScriptBlock.Ast } #GetScriptBlockAst elseif ($JobParams.ContainsKey('ScriptBlock')) { $ScriptBlockAst = $ScriptBlock.Ast } else { throw "Unknown ParameterSet" } #Identify NonLocal commands try { $LocalCommands = Get-AstStatement -Ast $ScriptBlockAst -Type FunctionDefinitionAst | Select-Object -ExpandProperty Name $NonLocalCommands = Get-AstStatement -Ast $ScriptBlockAst -Type CommandAst | ForEach-Object { $_.GetCommandName() } | Group-Object -NoElement | Select-Object -ExpandProperty Name | Where-Object { $LocalCommands -notcontains $_ } } catch { Write-Error "Identify NonLocal commands failed. Details: $_" -ErrorAction 'Stop' } #Resolve NonLocal commands against GlobalCommandAnalysis try { foreach ($cmd in $NonLocalCommands) { $CmdResult = [PSBuild.CommandAnalysis]::new() #Determine CmdName and Module if ($cmd.Contains('\')) { $tempcmd = $cmd -split '\\' $CmdResult.CommandSource = $tempcmd[0] $CmdResult.CommandName = $tempcmd[1] } else { $CmdResult.CommandName = $cmd } #Check if command is already analyzed if (-not $LocalCommandAnalysis.Contains($CmdResult.CommandName)) { #Resolve command Source if ($GlobalCommandAnalysis.Contains($CmdResult.CommandName)) { #Source already resolved $CmdResult = $GlobalCommandAnalysis[$CmdResult.CommandName] $CmdResult.IsReferenced = $false $CmdResult.IsFound = $true } else { #Check if command is local if (-not $CmdResult.IsFound) { try { $GetCommandParams = @{ Name = $CmdResult.CommandName } if ($CmdResult.CommandSource) { $SourceCandidate = $CmdResult.CommandSource } else { if ($JobParams.ContainsKey('CurrentDependancies')) { $SourceCandidate = $JobParams['CurrentDependancies'] } else { Remove-Variable -Name SourceCandidate -ErrorAction SilentlyContinue } } if (-not [string]::IsNullOrEmpty($CmdResult.CommandSource)) { $GetCommandParams.Add('Module', $CmdResult.CommandSource) } try { Remove-Variable -Name TempFindng -ErrorAction SilentlyContinue $TempFindng = Get-Command @GetCommandParams -ErrorAction Stop -Verbose:$false if ((-not $TempFindng) -and ($SourceCandidate)) { $TempFindng = Get-Command @GetCommandParams -Module $SourceCandidate -ErrorAction Stop -Verbose:$false } } catch { } if ($TempFindng) { $CmdResult.CommandType = $TempFindng.CommandType if ($TempFindng.CommandType -eq 'Alias') { $TempFinding2 = Get-Command -Name $TempFindng.Name -ErrorAction Stop -Verbose:$false $CmdResult.CommandSource = $TempFinding2.Source } else { $CmdResult.CommandSource = $TempFindng.Source } $CmdResult.IsFound = $true } } catch { Write-Error "Check if command is local failed. Details: $_" -ErrorAction 'Stop' } } } $null = $LocalCommandAnalysis.Add($CmdResult) } } } catch { Write-Error "Resolve NonLocal Commands against CommandToModuleMapping failed. Details: $_" -ErrorAction Stop } #Resolve Missing commands against Proget if ($JobParams.ContainsKey('PSGetRepository')) { try { $MissingCommandsAnalysis = $LocalCommandAnalysis | Where-Object { $_.IsFound -eq $false } if ($MissingCommandsAnalysis) { $AssertPSRepository_Params = @{ PSGetRepository = $JobParams['PSGetRepository'] } if ($JobParams.ContainsKey('Proxy')) { $AssertPSRepository_Params.Add('Proxy', $JobParams['Proxy']) } Assert-PSRepository @AssertPSRepository_Params -ErrorAction Stop foreach ($Repo in $JobParams['PSGetRepository']) { #Refresh MissingCommandsAnalysis $MissingCommandsAnalysis = $LocalCommandAnalysis | Where-Object { $_.IsFound -eq $false } if ($MissingCommandsAnalysis) { #Search for All MissingCommands $FindModule_Params = @{ Command = $MissingCommandsAnalysis.CommandName Repository = $Repo.Name } if ($Repo.ContainsKey('Credential')) { $FindModule_Params.Add('Credential', $Repo.Credential) } if ($JobParams.ContainsKey('Proxy')) { $FindModule_Params.Add('Proxy', $JobParams['Proxy']) } Remove-Variable -Name TempNugetResult -ErrorAction SilentlyContinue $private:TempNugetResult = Find-Module @FindModule_Params -ErrorAction Stop #Map search results to MissingCommands if ($TempNugetResult) { foreach ($MissingCmd in $MissingCommandsAnalysis) { $MissingCmdModule = $TempNugetResult | Where-Object { $_.Includes.Command -contains $MissingCmd.CommandName } if ($MissingCmdModule) { $LocalCommandAnalysis[$MissingCmd.CommandName].CommandSource = $MissingCmdModule.Name -join ',' $LocalCommandAnalysis[$MissingCmd.CommandName].CommandType = 'Command' $LocalCommandAnalysis[$MissingCmd.CommandName].SourceLocation = [PSBuild.CommandSourceLocation]::ProGet $LocalCommandAnalysis[$MissingCmd.CommandName].IsFound = $true } } } } } } } catch { Write-Error "Resolve Missing commands against Proget failed. Details: $_" -ErrorAction Stop } } #Check commands references try { foreach ($Cmd in ($LocalCommandAnalysis | Where-Object { $_.IsFound })) { if ($JobParams['CurrentDependancies'] -contains $cmd.CommandSource) { $cmd.IsReferenced = $true } } } catch { Write-Error "Check commands references failed. Details: $_" -ErrorAction Stop } #Update GlobalCommandAnalysis try { $FoundCommandsAnalysis = $LocalCommandAnalysis | Where-Object { $_.IsFound -eq $true } foreach ($FoundCmd in $FoundCommandsAnalysis) { if (-not $GlobalCommandAnalysis.Contains($FoundCmd.CommandName)) { $null = $GlobalCommandAnalysis.Add($FoundCmd) } } } catch { Write-Error "Update GlobalCommandAnalysis failed. Details: $_" -ErrorAction Stop } #Return result [pscustomobject]@{ LocalCommandAnalysisAsJson = $LocalCommandAnalysis.ToJson() GlobalCommandAnalysisAsJson = $GlobalCommandAnalysis.ToJson() } } $null = Wait-Job -Job $Job $JobResult = Receive-Job -Job $Job } finally { Remove-Job -Job $Job -Force } #Update CommandsToModuleMapping $null = [PSBuild.Context]::Current.CommandsToModuleMapping.TryAdd([PSBuild.CommandAnalysisCollection]::FromJson($JobResult.GlobalCommandAnalysisAsJson)) [PSBuild.CommandAnalysisCollection]::FromJson($JobResult.LocalCommandAnalysisAsJson) } } #> function priv_Test-Module { [CmdletBinding()] [OutputType([PSBuild.PSModuleValidation])] param ( [Parameter(Mandatory = $true)] [string]$ModulePath, [Parameter(Mandatory = $false)] [Version]$Version ) Process { $TestPsModule_Params = @{ ModulePath = $ModulePath ErrorAction = 'Stop' } if($PSBoundParameters.ContainsKey('Version')) { $TestPsModule_Params.Add('Version', $Version) } $testResult = Test-PSModule @TestPsModule_Params -Verbose:$false $wrappedResult = [PSBuild.PSModuleValidation]::new() $wrappedResult.ModuleInfo = $testResult.ModuleInfo $wrappedResult.IsModule = $testResult.IsModule $wrappedResult.IsVersionValid = $testResult.IsVersionValid $wrappedResult.IsNewVersion = $testResult.IsNewVersion $wrappedResult.SupportVersonControl = $testResult.SupportVersonControl $wrappedResult.IsReadyForPackaging = $testResult.IsReadyForPackaging $wrappedResult } } function priv_Test-Script { [CmdletBinding()] [OutputType([PSBuild.PSScriptValidation])] param ( [Parameter(Mandatory = $true)] [string]$ScriptPath, #UseScriptConfigFile [Parameter(Mandatory = $false)] [switch]$UseScriptConfigFile ) Process { $TestPsScript_Params = @{ ScriptPath = $ScriptPath UseScriptConfigFile = ($UseScriptConfigFile.IsPresent) ErrorAction = 'Stop' } $testResult = Test-PSScript @TestPsScript_Params -Verbose:$false $wrappedResult = [PSBuild.PSScriptValidation]::new(@{ ScriptInfo = $testResult.ScriptInfo ScriptConfig = $testResult.ScriptConfig IsScript = $testResult.IsScript IsVersionValid = $testResult.IsVersionValid IsNewVersion = $testResult.IsNewVersion SupportVersonControl = $testResult.SupportVersonControl IsReadyForPackaging = $testResult.IsReadyForPackaging ValidationErrors = $testResult.ValidationErrors }) $wrappedResult } } function priv_Build-SolutionModulesCache { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSBuild.PSModuleBuildInfo[]]$BuildConfiguration, [Parameter(Mandatory = $false)] [switch]$Clear ) Process { if($Clear) { [PSBuild.Context]::Current.SolutionModulesCache.Clear() } foreach ($ModBuildCfg in $BuildConfiguration) { $testResult = priv_Test-Module -ModulePath ($ModBuildCfg.SourcePath) if($testResult.IsModule) { $testResult.TargetDirectory = $ModBuildCfg.DestinationPath [PSBuild.Context]::Current.SolutionModulesCache.Update($testResult) } else { throw "Build Solution Module Cache failed. Unsupported operation: missing ModuleInfo for module $($ModBuildCfg.Name)." } } } } function priv_Build-SolutionScriptsCache { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSBuild.PSScriptBuildInfo[]]$BuildConfiguration, #UseScriptConfigFile [Parameter(Mandatory = $false)] [switch]$UseScriptConfigFile, [Parameter(Mandatory = $false)] [switch]$Clear ) Process { if($Clear) { [PSBuild.Context]::Current.SolutionScriptsCache.Clear() } foreach ($ScrBuildCfg in $BuildConfiguration) { Write-Verbose -Message "[Build PSScript] Analyzing '$($ScrBuildCfg.Name)'" $TestScript_Params = @{ ScriptPath = ($ScrBuildCfg.SourcePath) UseScriptConfigFile = ($UseScriptConfigFile.IsPresent) } $testResult = priv_Test-Script @TestScript_Params if($testResult.IsScript) { $testResult.TargetDirectory = $ScrBuildCfg.DestinationPath $testResult.RequiredModulesTargetDirectory = $ScrBuildCfg.RequiredModulesDestinationPath [PSBuild.Context]::Current.SolutionScriptsCache.Update($testResult) } else { $ErrorMsg = "Build Solution Scripts Cache failed." if($testResult.ValidationErrors) { $ErrorMsg += [System.Environment]::NewLine $ErrorMsg += "Validation Errors:" $ErrorMsg += [System.Environment]::NewLine $ErrorMsg += $testResult.ValidationErrors -join "$([System.Environment]::NewLine)" } else { $ErrorMsg += " Unsupported operation: missing ScriptInfo for script $($ScrBuildCfg.FullName)." } throw $ErrorMsg } } } } function priv_Build-CommandsToModuleMapping { [CmdletBinding()] [OutputType([void])] param ( [Parameter(Mandatory = $false)] [switch]$Clear ) Process { if($Clear) { [PSBuild.Context]::Current.CommandsToModuleMapping.Clear() } # Validate Native Commands try { #Add BuiltIn Comands and Aliases to CommandsToModuleMapping (from module 'Microsoft.PowerShell.Core' as they are BuiltIn to PowerShell) foreach($psNativeModule in $PSNativeModules) { [PSModuleInfo]$psNativeModuleObj = Get-Module -Name $psNativeModule -ListAvailable -Verbose:$false -ErrorAction Stop | Select-Object -First 1 if($psNativeModuleObj) { [PSBuild.Context]::Current.ExternalModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($psNativeModuleObj.Name, $psNativeModuleObj.Version, $null, $psNativeModuleObj.Path)) } else { $psNativeSnapInObj = Get-PSSnapin -Name $psNativeModule -Verbose:$false -ErrorAction SilentlyContinue | Select-Object -First 1 if($psNativeSnapInObj) { [PSBuild.Context]::Current.ExternalModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($psNativeSnapInObj.Name, $psNativeSnapInObj.Version, $null, $null)) } else { Write-Warning -Message "Module '$psNativeModule' is marked as a required native module but it could not be found." } $psNativeSnapInObj = $null } $psNativeModuleObj = $null } #This also imports the module in the current session Get-Command -Module $PSNativeModules | ForEach-Object { #Add Command $Command = [PSBuild.CommandSource]::new($_, [PSBuild.CommandSourceLocation]::BuiltIn) if ($Command.CommandType -eq 'Alias') { try { $currentVerbosePref = $VerbosePreference $VerbosePreference = "SilentlyContinue" $TempFinding2 = Get-Command -Name $Command.CommandName -ErrorAction Stop $Command.Source = $TempFinding2.Source $VerbosePreference = $currentVerbosePref } catch { } } #Do not validate duplicates here $null = [PSBuild.Context]::Current.CommandsToModuleMapping.AddCommandSource($Command, $false) #Add Alias if exists Get-Alias -Definition $_.Name -ErrorAction SilentlyContinue | foreach { $Alias = [PSBuild.CommandSource]::new($_, [PSBuild.CommandSourceLocation]::BuiltIn) $Alias.Source = $Command.Source $null = [PSBuild.Context]::Current.CommandsToModuleMapping.AddCommandSource($Alias, $false) } } } catch { Write-Error "Validate Native Commands failed. Details: $_" -ErrorAction 'Stop' } # Add Solution Commands to CommandsToModuleMapping (from all modules in the solution) foreach ($ModuleObj in [PSBuild.Context]::Current.SolutionModulesCache) { priv_Update-CommandsToModuleMapping -ModuleInfo ($ModuleObj.ModuleInfo) -CommandSourceLocation Solution } # Add External Dependency Commands to CommandsToModuleMapping foreach($ModuleObj in [PSBuild.Context]::Current.SolutionModulesCache) { foreach($RequiredModuleExt in $ModuleObj.GetExternalModuleDependencies()) { if(-not ([PSBuild.Context]::Current.ExternalModulesCache.Contains($RequiredModuleExt.Name))) { # Search default PS locations $ResolvedExtModuleObj = $null $ResolvedExtModuleObj = Get-Module -Name ($RequiredModuleExt.Name) -ListAvailable -Refresh -ErrorAction SilentlyContinue -Verbose:$false | Sort-Object -Property Version | Select-Object -Last 1 if($ResolvedExtModuleObj) { priv_Update-CommandsToModuleMapping -ModuleInfo $ResolvedExtModuleObj -CommandSourceLocation Unknown [PSBuild.Context]::Current.ExternalModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ResolvedExtModuleObj.Name, $ResolvedExtModuleObj.Version, $null, $ResolvedExtModuleObj.Path)) } else { # Try resolve module by path if(Test-Path -Path ($RequiredModuleExt.Path)) { $ResolvedExtModuleObj = Get-Module -Name ($RequiredModuleExt.Path) -ListAvailable -Refresh -ErrorAction SilentlyContinue -Verbose:$false | Sort-Object -Property Version | Select-Object -Last 1 } if($ResolvedExtModuleObj) { priv_Update-CommandsToModuleMapping -ModuleInfo $ResolvedExtModuleObj -CommandSourceLocation Unknown $mbi = [PSBuild.PSModuleBuildInfo]::new($ResolvedExtModuleObj.Name, $ResolvedExtModuleObj.Version, $null, $ResolvedExtModuleObj.Path) $mbi.IsPortableModule = $false [PSBuild.Context]::Current.ExternalModulesCache.Add($mbi) } else { throw "Failed to resolve External Dependencies for Module '$ModuleObj'. Required module '$RequiredModuleExt' was not found." } } } } } } } function priv_Update-CommandsToModuleMapping { [CmdletBinding()] [OutputType([void])] param ( [Parameter(Mandatory = $true)] [PSModuleInfo]$ModuleInfo, [Parameter(Mandatory = $false)] [PSBuild.CommandSourceLocation]$CommandSourceLocation = [PSBuild.CommandSourceLocation]::Unknown ) Process { #Validate Module Commands try { foreach ($cmd in $ModuleInfo.ExportedCommands.Values) { $Command = [PSBuild.CommandSource]::new($cmd, $CommandSourceLocation) # The command already exists in the collection if([PSBuild.Context]::Current.CommandsToModuleMapping.Contains($Command.CommandName)) { if([PSBuild.Context]::Current.CommandsToModuleMapping[$Command.CommandName].ContainsSource($Command.Source)) { # This module is already present in the collection } elseif (-not [PSBuild.Context]::Current.AllowDuplicateCommandsInCommandToModuleMapping) { throw "Command with name '$($Command.CommandName)' is present in multiple modules: $([PSBuild.Context]::Current.CommandsToModuleMapping[$Command.CommandName].CommandSources[0].Source), $($Command.Source)" } else { $null = [PSBuild.Context]::Current.CommandsToModuleMapping.AddCommandSource($Command) } } # The command is new else { $null = [PSBuild.Context]::Current.CommandsToModuleMapping.AddCommandSource($Command) } } } catch { Write-Error "Validate Module Commands failed. Details: $_" -ErrorAction 'Stop' } } } function priv_Save-RequiredNugetModules { [CmdletBinding()] [OutputType([void])] param ( [Parameter(Mandatory = $true)] [PSBuild.RequiredModuleSpecs[]]$RequiredModules, #PSGetRepository [Parameter(Mandatory = $true)] [hashtable[]]$PSGetRepository, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { ### FIND ITEMS (At this step the goal is to know which repository hosts the required module.) foreach($ReqModule in $RequiredModules) { if([PSBuild.Context]::Current.PsGetModuleValidationCache.Contains($ReqModule.Name, $ReqModule.Version)) { # This may happen if you call the Build-PSModule function twice Write-Verbose -Message "[Build PSModule] Skipping module $ReqModule (already resolved)" ### NOTE: This updates the original object - therefore the caller function will see the updates !!! $ReqModule.SourceInformation = [PSBuild.Context]::Current.PsGetModuleValidationCache.Find({param($mvc) process{($mvc.Name -eq $ReqModule.Name) -and ($mvc.Version -eq $ReqModule.Version)}}) } else { $tempCollection = [PSBuild.PSRepositoryItemValidationCollection]::new() foreach($Repo in $PSGetRepository) { $PSGet_Params = @{ Name = ($ReqModule.Name) Repository = ($Repo.Name) } if (-not ([PSBuild.Context]::Current.UpdateModuleReferences)) { $PSGet_Params.Add('RequiredVersion', $ReqModule.Version) } if ($Repo.ContainsKey('Credential')) { $PSGet_Params.Add('Credential', $Repo.Credential) } if ($PSBoundParameters.ContainsKey('Proxy')) { $PSGet_Params.Add('Proxy', $Proxy) } try { $PSRepoItem = Find-Module @PSGet_Params -ErrorAction Stop -Verbose:$false if($PSRepoItem) { $tempCollection.Add([PSBuild.PSRepositoryItemValidation]::new(@{ Dependencies = $PSRepoItem.Dependencies Includes = $PSRepoItem.Includes Name = $PSRepoItem.Name Repository = $PSRepoItem.Repository Type = $PSRepoItem.Type Version = $PSRepoItem.Version PackageManagementProvider = $PSRepoItem.PackageManagementProvider RepositorySourceLocation = $PSRepoItem.RepositorySourceLocation Priority = $Repo.Priority Credential = $Repo.Credential })) Write-Verbose -Message "[Build PSModule] Module $ReqModule found in repository $($Repo.Name)" } } catch { # Module not found in repo } } if($tempCollection.Count -eq 0) { Write-Warning -Message "Module $ReqModule was not found in registered repositories. The build process may still succeed if the module was previously built in the same destination folder." } elseif($tempCollection.Count -eq 1) { ### NOTE: This updates the original object - therefore the caller function will see the updates !!! $ReqModule.SourceInformation = $tempCollection[0] [PSBuild.Context]::Current.PsGetModuleValidationCache.Add($ReqModule.SourceInformation) } else { # If a module version is found in multiple repositories, use only the repo with higher priority ### NOTE: This updates the original object - therefore the caller function will see the updates !!! $ReqModule.SourceInformation = $($tempCollection | Sort-Object -Property Priority | Select-Object -First 1) [PSBuild.Context]::Current.PsGetModuleValidationCache.Add($ReqModule.SourceInformation) } $tempCollection = $null } } ## SAVE ITEMS Write-Verbose -Message "[Build PSModule] Downloading modules from repositories..." foreach($ReqModule in $RequiredModules) { if(-not [IO.Directory]::Exists($ReqModule.TargetDirectory)) { $null = [IO.Directory]::CreateDirectory($ReqModule.TargetDirectory) } $TargetBuildPath = [IO.Path]::Combine($ReqModule.TargetDirectory, ($ReqModule.SourceInformation.Name)) $ExistingModuleTest = $null $ExistingModuleTest = priv_Test-Module -ModulePath $TargetBuildPath -Version ($ReqModule.SourceInformation.Version) if($ExistingModuleTest.IsModule -and (( -not ($ExistingModuleTest.SupportVersonControl)) -or ($ExistingModuleTest.IsVersionValid))) { Write-Verbose -Message "[Build PSModule] Skipping module $ExistingModuleTest - a valid version already exists at target location." [PSBuild.Context]::Current.BuiltModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ExistingModuleTest.ModuleName, $ExistingModuleTest.ModuleInfo.Version, $null, $ReqModule.TargetDirectory)) priv_Update-CommandsToModuleMapping -ModuleInfo ($ExistingModuleTest.ModuleInfo) -CommandSourceLocation ProGet } elseif($ReqModule.SourceInformation) { Write-Verbose -Message "[Build PSModule] Downloading $($ReqModule.SourceInformation) (Repository: $($ReqModule.SourceInformation.Repository))" $SaveModule_Params = @{ Name = $ReqModule.SourceInformation.Name Repository = $ReqModule.SourceInformation.Repository RequiredVersion = $ReqModule.SourceInformation.Version Path = $ReqModule.TargetDirectory ErrorAction = 'Stop' Verbose = $false } if ($ReqModule.SourceInformation.Credential) { $SaveModule_Params.Add('Credential', $ReqModule.SourceInformation.Credential) } if ($PSBoundParameters.ContainsKey('Proxy')) { $SaveModule_Params.Add('Proxy', $Proxy) } Save-Module @SaveModule_Params $ExistingModuleTest = priv_Test-Module -ModulePath $TargetBuildPath -Version ($ReqModule.SourceInformation.Version) -Verbose:$false if($ExistingModuleTest.IsModule) { if($ExistingModuleTest.SupportVersonControl -and ( -not ($ExistingModuleTest.IsVersionValid))) { # There is not much we can do for modules that are downloaded from repos. Show a warning and hope for the best :) Write-Warning -Message "$ExistingModuleTest was successfully downloaded but it was detected that there are inconsistencies with the module's version control. Make sure the module is valid." } [PSBuild.Context]::Current.BuiltModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ExistingModuleTest.ModuleName, $ExistingModuleTest.ModuleInfo.Version, $null, $ReqModule.TargetDirectory)) priv_Update-CommandsToModuleMapping -ModuleInfo ($ExistingModuleTest.ModuleInfo) -CommandSourceLocation ProGet } else { throw "The module $ExistingModuleTest downloaded from $($ReqModule.SourceInformation.Repository) failed module validations." } } else { throw "The solution requires external module $ReqModule but it was not found in any repository." } } } } #endregion #region Public Functions function Build-PSModule { [CmdletBinding()] [OutputType([void])] param ( #SourcePath [Parameter(Mandatory = $true, ParameterSetName = 'BySourceAndDestinationPaths')] [System.IO.DirectoryInfo[]]$SourcePath, #DestinationPath [Parameter(Mandatory = $true, ParameterSetName = 'BySourceAndDestinationPaths')] [System.IO.DirectoryInfo]$DestinationPath, #ModulePathConfiguration [Parameter(Mandatory = $true, ParameterSetName = 'ByModulePathConfiguration')] [PSBuild.PSModuleBuildInfo[]]$ModulePathConfiguration, #ResolveDependancies [Parameter(Mandatory = $false)] [switch]$ResolveDependancies, #CheckCommandReferencesConfiguration [Parameter(Mandatory = $false)] [PSBuild.CheckCommandReferencesConfiguration]$CheckCommandReferencesConfiguration, #CheckDuplicateCommandNames [Parameter(Mandatory = $false)] [switch]$CheckDuplicateCommandNames, #UpdateModuleReferences [Parameter(Mandatory = $false)] [switch]$UpdateModuleReferences, #PSGetRepository [Parameter(Mandatory = $false)] [hashtable[]]$PSGetRepository, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { ### PART 1: Prerequisites Write-Verbose -Message "[Build PSModule] Preparing Prerequisites..." $ModuleBuildCfg = [PSBuild.PSModuleBuildInfoCollection]::new() $ModuleDestinationList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) if($PsCmdlet.ParameterSetName -eq 'BySourceAndDestinationPaths') { foreach($srcPath in $SourcePath) { $ModuleBuildCfg.Add([PSBuild.PSModuleBuildInfo]::new( $srcPath.Name, $null, $srcPath.FullName, $DestinationPath.FullName )) } $null = $ModuleDestinationList.Add($DestinationPath.FullName) } else { foreach($modPathCfg in $ModulePathConfiguration) { $ModuleBuildCfg.Add($modPathCfg) $null = $ModuleDestinationList.Add($modPathCfg.DestinationPath) } } # Update PSModulePath environmental variable Add-PSModulePathEntry -Path $ModuleDestinationList -Scope Process -Force # Assert PSRepositories if ($PSBoundParameters.ContainsKey('PSGetRepository')) { $AssertPSRepository_Params = @{ PSGetRepository = $PSGetRepository } if ($PSBoundParameters.ContainsKey('Proxy')) { $AssertPSRepository_Params.Add('Proxy', $Proxy) } Assert-PSRepository @AssertPSRepository_Params -ErrorAction Stop } # Update Command References Config if($PSBoundParameters.ContainsKey('CheckDuplicateCommandNames')) { [PSBuild.Context]::Current.AllowDuplicateCommandsInCommandToModuleMapping = (-not ($CheckDuplicateCommandNames.IsPresent)) } # Update Module References Config if($PSBoundParameters.ContainsKey('UpdateModuleReferences')) { [PSBuild.Context]::Current.UpdateModuleReferences = ($UpdateModuleReferences.IsPresent) } # Update Command References Config if($PSBoundParameters.ContainsKey('CheckCommandReferencesConfiguration')) { [PSBuild.Context]::Current.CheckCommandReferencesConfiguration = $CheckCommandReferencesConfiguration } ### PART 2: Build Cache Data #Validate All Modules priv_Build-SolutionModulesCache -BuildConfiguration $ModuleBuildCfg priv_Build-CommandsToModuleMapping ### PART 3: Analyze modules in bulk and find all required modules that are not part of this solution. Write-Verbose -Message "[Build PSModule] Analyzing Module Dependencies..." $SolutionPSGetDependencies = [PSBuild.RequiredModuleSpecsCollection]::new() foreach($ModuleObj in [PSBuild.Context]::Current.SolutionModulesCache) { $SolutionPSGetDependencies.AddRange($ModuleObj.GetRequiredModules([PSBuild.RequiredModulesFilterOption]::RemoveExternalDependenciesAndKnownSolutionItems)) } if($SolutionPSGetDependencies.Count -gt 0) { if([PSBuild.Context]::Current.UpdateModuleReferences) { $SolutionPSGetDependencies = $SolutionPSGetDependencies.GetLatestModuleVersions() } else { $SolutionPSGetDependencies = $SolutionPSGetDependencies.GetUniqueModuleVersions() } Write-Verbose -Message "[Build PSModule] Detected $($SolutionPSGetDependencies.Count) required modules outside solution. Searching Repositories..." if (!$PSBoundParameters.ContainsKey('PSGetRepository')) { throw "Cannot resolve dependencies. It appears that some modules are not part of the solution but there were no specified search repositories from which to download them." } $SaveRequiredModules_Params = @{ RequiredModules = $SolutionPSGetDependencies PSGetRepository = $PSGetRepository } if ($PSBoundParameters.ContainsKey('Proxy')) { $SaveRequiredModules_Params.Add('Proxy', $Proxy) } priv_Save-RequiredNugetModules @SaveRequiredModules_Params } ### PART 4: Build Local Modules. The processing order ensures dependencies are built first. Write-Verbose -Message "[Build PSModule] Building Local Solution Modules..." foreach ($ModuleObj in [PSBuild.Context]::Current.SolutionModulesCache.GetOrderedProcessingList()) { if($ModuleObj.PreferredProcessingOrder -gt 1000) { throw "Detected cyclic dependency at module $ModuleObj." } # PART 4-a: Analyze Required Modules # This time we expect that all required modules have already been downloaded from repos or built in advance # If a required module is not at the target build location, there is an unresolvable dependency and the process stops. Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Validating Dependencies" foreach($requiredModule in $ModuleObj.GetRequiredModules([PSBuild.RequiredModulesFilterOption]::RemoveExternalDependencies)) { if([PSBuild.Context]::Current.BuiltModulesCache.Contains(@{ Name=$requiredModule.Name; DestinationPath=$requiredModule.TargetDirectory })) { # Already built # At this point the built module may be the same version or newer } else { $TargetBuildPath = [IO.Path]::Combine($requiredModule.TargetDirectory, $requiredModule.Name) $ExistingModuleTest = $null $ExistingModuleTest = priv_Test-Module -ModulePath $TargetBuildPath if($ExistingModuleTest.IsModule -and ($ExistingModuleTest.ModuleInfo.Version -ge $requiredModule.Version)) { if($ExistingModuleTest.SupportVersonControl -and (-not ($ExistingModuleTest.IsVersionValid))) { # The same warning as in Step 2. Assumption is it was manually placed there or built in another process, so show the warning again: Write-Warning -Message "$ExistingModuleTest was found in the target build location but it was detected that there are inconsistencies with the module's version control. Make sure the module is valid." } [PSBuild.Context]::Current.BuiltModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ExistingModuleTest.ModuleName, $ExistingModuleTest.ModuleInfo.Version, $null, $requiredModule.TargetDirectory)) } elseif([PSBuild.Context]::Current.SolutionModulesCache.Contains($requiredModule.Name)) { # This is usually caused by a circular dependency. The Solution Modules are processed in order which ensures all required modules are built in advance. # If this module is missing, the dependency tree is invalid throw "Build module $ModuleObj failed. Cannot resolve module dependencies. The required module $requiredModule was not found in the build directory. This problem may be caused by a circular dependency." } else { # Note that this is a required module, not found in the solution and not found in the ps repos. # This was a best effort attempt to find it in the build directory and use it. throw "Build module $ModuleObj failed. Cannot resolve module dependencies. The required module $requiredModule is not part of the solution and was not found in any of the provided repositories." } } } # PART 4-b: Update Required Modules versions in the Module Manifest Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Validating Required Module Versions" $ModuleDependanciesDefinition = New-Object -TypeName System.Collections.ArrayList $UpdateModuleManifestRequired = $false foreach($requiredModule in $ModuleObj.ModuleInfo.RequiredModules) { $shouldUpdateVersion = $false $latestVersion = $null $reqModuleName = $requiredModule.Name if([PSBuild.Context]::Current.BuiltModulesCache.Contains($requiredModule.Name)) { $latestVersion = [PSBuild.Context]::Current.BuiltModulesCache.GetLatestVersion($requiredModule.Name) if(($requiredModule.Version -lt $latestVersion.Version) -and ([PSBuild.Context]::Current.UpdateModuleReferences -or [PSBuild.Context]::Current.SolutionModulesCache.Contains($requiredModule.Name))) { $shouldUpdateVersion = $true } } elseif([PSBuild.Context]::Current.ExternalModulesCache.Contains($requiredModule.Name)) { $latestVersion = [PSBuild.Context]::Current.ExternalModulesCache.GetLatestVersion($requiredModule.Name) if($requiredModule.Version -lt $latestVersion.Version) { Write-Warning -Message "Module $ModuleObj depends on $requiredModule which is not part of this solution. $($requiredModule.Name) has a newer version ($($latestVersion.Version)) and the required module reference will be updated." $shouldUpdateVersion = $true } if(-not ($latestVersion.IsPortableModule)) { $reqModuleName = $latestVersion.DestinationPath } } else { throw "Required module $($requiredModule.Name) was not found in either the Built Modules Cache or the External Modules Cache. This should not be possible but you still did it! Congrats!" } if($shouldUpdateVersion) { $null = $ModuleDependanciesDefinition.Add([Microsoft.PowerShell.Commands.ModuleSpecification]::new(@{ ModuleName = $reqModuleName ModuleVersion = $latestVersion.Version })) Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Required Module '$($requiredModule.Name)/$($requiredModule.Version)' has a newer version ($($latestVersion.Version))" $UpdateModuleManifestRequired = $true } else { $null = $ModuleDependanciesDefinition.Add([Microsoft.PowerShell.Commands.ModuleSpecification]::new(@{ ModuleName = $reqModuleName ModuleVersion = $requiredModule.Version })) } } if ($UpdateModuleManifestRequired) { Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Updating Module Manifest" $currentVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' Update-ModuleManifest -Path ($ModuleObj.ModuleInfo.Path) -RequiredModules $ModuleDependanciesDefinition -ErrorAction Stop -Verbose:$false $VerbosePreference = $currentVerbosePreference } ### PART 4-c: Check if all commands are referenced if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.Enabled) { # Update Module Definition. This is required in order to extract all commands used inside the module # Note that this is required independent of the fact that the module may be in the exclusion list because there may be another module which depends on this module and its validations will fail Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Reading Module AST" $modDefiniton = priv_Get-ModuleDefinition -ModulePath ($ModuleObj.SourceDirectory) -DestinationPath $ModuleDestinationList -ProactiveRequiredModuleLoading ($ModuleObj.PreferredProcessingOrder -gt 2) $ModuleObj.UpdateModuleDefinitionAst($modDefiniton) if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.ExcludedSources -notcontains ($ModuleObj.ModuleName)) { Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Checking if all commands are referenced" foreach($NonLocalCommand in $ModuleObj.GetNonLocalCommands()) { if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.ExcludedCommands -notcontains $NonLocalCommand) { if([PSBuild.Context]::Current.CommandsToModuleMapping.ContainsCommand($NonLocalCommand)) { $cmdIsReferenced = $false foreach($cmdSrc in ([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand))) { if(($cmdSrc.SourceLocation -eq [PSBuild.CommandSourceLocation]::BuiltIn) -or ($ModuleObj.GetRequiredModules([PSBuild.RequiredModulesFilterOption]::FindAll).Name -contains $cmdSrc.Source)) { # Module is either BuiltIn or explicitly referenced $cmdIsReferenced = $true break } } if(-not $cmdIsReferenced) { if([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand).Count -gt 1) { throw "Build Module '$ModuleObj' failed. The command '$NonLocalCommand' was found in modules $(@([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand).ForEach{ "'$($_.Source)'" }) -join ', ') but neither is referenced in the module manifest." } else { throw "Build Module '$ModuleObj' failed. The command '$NonLocalCommand' was found in module '$([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand)[0].Source)' but it is not referenced in the module manifest." } } } else { throw "Build Module '$ModuleObj' failed. The command '$NonLocalCommand' was not found in any of the required modules referenced in the module manifest." } } } } } ### PART 4-d: Check Module Integrity if (-not $ModuleObj.IsReadyForPackaging) { throw "Build Module '$ModuleObj' failed. The module is not ready for packaging. Missing either Author or Description." } if (-not $ModuleObj.IsValid) { ## NOTE: This also updates the file hash Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Updating Module Version" $currentVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $oldModuleVer = $ModuleObj.ModuleInfo.Version Update-PSModuleVersion -ModulePath ($ModuleObj.SourceDirectory) -ErrorAction Stop -Verbose:$false #Refresh ModuleValidation $ModuleObj.Update($(priv_Test-Module -ModulePath ($ModuleObj.SourceDirectory) -ErrorAction Stop)) Write-Warning -Message "[Build PSModule] Processing $ModuleObj -> Updated module from version $($oldModuleVer) to version $($ModuleObj.ModuleInfo.Version)" $VerbosePreference = $currentVerbosePreference } ### PART 4-e: Export Module to DestinationPath Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> Exporting Module" try { priv_Export-Artifact -Type Module -SourcePath ($ModuleObj.SourceDirectory) -Version ($ModuleObj.ModuleInfo.Version) -DestinationPath ($ModuleObj.TargetDirectory) -Verbose:$false [PSBuild.Context]::Current.BuiltModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ModuleObj.ModuleName, $ModuleObj.ModuleInfo.Version, $ModuleObj.SourceDirectory, $ModuleObj.TargetDirectory)) } catch { throw "Build Module '$ModuleObj' failed. Cannot copy module to '$DestinationPath'. details: $_" } Write-Verbose -Message "[Build PSModule] Processing $ModuleObj -> DONE" } } } function Build-PSScript { [CmdletBinding()] [OutputType([void])] param ( #SourcePath [Parameter(Mandatory = $true, ParameterSetName = 'BySourceAndDestinationPaths')] [System.IO.FileInfo[]]$SourcePath, #DestinationPath [Parameter(Mandatory = $false, ParameterSetName = 'BySourceAndDestinationPaths')] [System.IO.DirectoryInfo]$DestinationPath, #DependencyDestinationPath [Parameter(Mandatory = $false, ParameterSetName = 'BySourceAndDestinationPaths')] [System.IO.DirectoryInfo]$DependencyDestinationPath, #ModulePathConfiguration [Parameter(Mandatory = $true, ParameterSetName = 'ByModulePathConfiguration')] [PSBuild.PSScriptBuildInfo[]]$ScriptPathConfiguration, #UseScriptConfigFile [Parameter(Mandatory = $false)] [switch]$UseScriptConfigFile, #ResolveDependancies [Parameter(Mandatory = $false)] [switch]$ResolveDependancies, #CheckCommandReferencesConfiguration [Parameter(Mandatory = $false)] [PSBuild.CheckCommandReferencesConfiguration]$CheckCommandReferencesConfiguration, #UpdateModuleReferences [Parameter(Mandatory = $false)] [switch]$UpdateModuleReferences, #PSGetRepository [Parameter(Mandatory = $false)] [hashtable[]]$PSGetRepository, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { ### PART 1: Prerequisites Write-Verbose -Message "[Build PSScript] Preparing Prerequisites..." $ScriptBuildCfg = [PSBuild.PSScriptBuildInfoCollection]::new() $ModuleDestinationList = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) if($PsCmdlet.ParameterSetName -eq 'BySourceAndDestinationPaths') { if(($ResolveDependancies.IsPresent) -and [String]::IsNullOrEmpty($DestinationPath.FullName) -and [String]::IsNullOrEmpty($DependencyDestinationPath.FullName)) { throw "Build Solution Scripts failed. The Solution Configuration is invalid. ScriptPath requires you to specify at least one of the following properties: BuildPath, DependencyDestinationPath." } foreach($srcPath in $SourcePath) { $ScriptBuildCfg.Add([PSBuild.PSScriptBuildInfo]::new(@{ Name = $srcPath.Name SourcePath = $srcPath.FullName DestinationPath = $DestinationPath.FullName RequiredModulesDestinationPath = $(if($DependencyDestinationPath.FullName) {$DependencyDestinationPath.FullName} else {$DestinationPath.FullName}) })) } if($DestinationPath.FullName) { $null = $ModuleDestinationList.Add($DestinationPath.FullName) } if($DependencyDestinationPath.FullName) { $null = $ModuleDestinationList.Add($DependencyDestinationPath.FullName) } } else { foreach($scrPathCfg in $ScriptPathConfiguration) { $ScriptBuildCfg.Add($scrPathCfg) if($scrPathCfg.DestinationPath) { $null = $ModuleDestinationList.Add($scrPathCfg.DestinationPath) } if($scrPathCfg.RequiredModulesDestinationPath) { $null = $ModuleDestinationList.Add($scrPathCfg.RequiredModulesDestinationPath) } } } # Update PSModulePath environmental variable if($ResolveDependancies.IsPresent) { Add-PSModulePathEntry -Path $ModuleDestinationList -Scope Process -Force } # Assert PSRepositories if ($PSBoundParameters.ContainsKey('PSGetRepository')) { $AssertPSRepository_Params = @{ PSGetRepository = $PSGetRepository } if ($PSBoundParameters.ContainsKey('Proxy')) { $AssertPSRepository_Params.Add('Proxy', $Proxy) } Assert-PSRepository @AssertPSRepository_Params -ErrorAction Stop } # Update Command References Config if($PSBoundParameters.ContainsKey('CheckDuplicateCommandNames')) { [PSBuild.Context]::Current.AllowDuplicateCommandsInCommandToModuleMapping = (-not ($CheckDuplicateCommandNames.IsPresent)) } # Update Module References Config if($PSBoundParameters.ContainsKey('UpdateModuleReferences')) { [PSBuild.Context]::Current.UpdateModuleReferences = ($UpdateModuleReferences.IsPresent) } # Update Command References Config if($PSBoundParameters.ContainsKey('CheckCommandReferencesConfiguration')) { [PSBuild.Context]::Current.CheckCommandReferencesConfiguration = $CheckCommandReferencesConfiguration } ### PART 2: Build Cache Data Write-Verbose -Message "[Build PSScript] Building Solution Scripts Cache..." priv_Build-SolutionScriptsCache -BuildConfiguration $ScriptBuildCfg -UseScriptConfigFile:($UseScriptConfigFile.IsPresent) ### PART 3: Analyze Dependencies Write-Verbose -Message "[Build PSScript] Analyzing Dependencies..." $SolutionPSGetDependencies = [PSBuild.RequiredModuleSpecsCollection]::new() $ExternalModuleDependencies = [PSBuild.RequiredModuleSpecsCollection]::new() foreach($ScriptObj in [PSBuild.Context]::Current.SolutionScriptsCache) { $allScrReqModules = @(if($UseScriptConfigFile.IsPresent) { $ScriptObj.ScriptConfig.RequiredModules } else { $ScriptObj.ScriptInfo.RequiredModules }) foreach($reqModule in $allScrReqModules) { # At this point the required module object is the correct type but it hasn't been populated with the correct target directory yet $reqModule.TargetDirectory = $ScriptObj.RequiredModulesTargetDirectory if([PSBuild.Context]::Current.SolutionModulesCache.Contains($reqModule.Name)) { if([PSBuild.Context]::Current.BuiltModulesCache.Contains($reqModule.Name)) { # If object is in the Built Modules Cache, it has been built in the previous step, so just skip it to save time # Note: here we demand that it is built in the CORRECT directory if([PSBuild.Context]::Current.BuiltModulesCache.Contains(@{ Name=$reqModule.Name; DestinationPath=$reqModule.TargetDirectory })) { Write-Verbose -Message "[Build PSScript] Analyzing '$($ScriptObj.Name)' -> Skipping required module '$reqModule' because it has already been built." } else { # This module has definitely been built previously because it is in the Solution Modules Cache # The module, however, is not at the required Target Directory which is a problem with the Solution Configuration and not the build process # Throw error here and demand that Build-PSModule uses the same target dir as required for the scripts throw "Build PSScript Failed. The script '$ScriptObj' requires module '$($reqModule.Name)' which appears to be built in an incorrect location. Make sure to build solution modules in the correct destination directory. The Script Dependencies folder is '$($reqModule.TargetDirectory)'." } } else { throw "Build PSScript Failed. The script '$ScriptObj' requires module '$($reqModule.Name)' which appears to be part of the solution but for some reason it is not built yet. This is an unsupported scenario. All required modules should be built in advance using the Build-PSModule command." } } elseif($ScriptObj.ScriptInfo.ExternalModuleDependencies -contains $reqModule.Name) { $ExternalModuleDependencies.Add($reqModule) } else { $SolutionPSGetDependencies.Add($reqModule) } } } $ExternalModuleDependencies = $ExternalModuleDependencies.GetLatestModuleVersions() # Always use latest external modules $SolutionPSGetDependencies = if([PSBuild.Context]::Current.UpdateModuleReferences) { $SolutionPSGetDependencies.GetLatestModuleVersions() } else { $SolutionPSGetDependencies.GetUniqueModuleVersions() } # Download Nuget Modules if($SolutionPSGetDependencies.Count -gt 0) { Write-Verbose -Message "[Build PSScript] Detected $($SolutionPSGetDependencies.Count) required modules outside solution. Searching Repositories..." if (!$PSBoundParameters.ContainsKey('PSGetRepository')) { throw "Cannot resolve dependencies. It appears that some modules are not part of the solution but there were no specified search repositories from which to download them." } $SaveRequiredModules_Params = @{ RequiredModules = $SolutionPSGetDependencies PSGetRepository = $PSGetRepository } if ($PSBoundParameters.ContainsKey('Proxy')) { $SaveRequiredModules_Params.Add('Proxy', $Proxy) } priv_Save-RequiredNugetModules @SaveRequiredModules_Params } # Check External Dependencies # Here we are only listing the exported commands, which will be used during the Command Analysis phase later. Nothing to build. if($ExternalModuleDependencies.Count -gt 0) { foreach($RequiredModuleExt in $ExternalModuleDependencies) { if(-not [PSBuild.Context]::Current.ExternalModulesCache.Contains($RequiredModuleExt.Name)) { # Search default PS locations $ResolvedExtModuleObj = $null $ResolvedExtModuleObj = Get-Module -Name ($RequiredModuleExt.Name) -ListAvailable -Refresh -ErrorAction SilentlyContinue -Verbose:$false | Sort-Object -Property Version | Select-Object -Last 1 if($ResolvedExtModuleObj) { priv_Update-CommandsToModuleMapping -ModuleInfo $ResolvedExtModuleObj -CommandSourceLocation Unknown [PSBuild.Context]::Current.ExternalModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ResolvedExtModuleObj.Name, $ResolvedExtModuleObj.Version, $null, $ResolvedExtModuleObj.Path)) } else { # Try resolve module by path if(Test-Path -Path ($RequiredModuleExt.Path)) { $ResolvedExtModuleObj = Get-Module -Name ($RequiredModuleExt.Path) -ListAvailable -Refresh -ErrorAction SilentlyContinue -Verbose:$false | Sort-Object -Property Version | Select-Object -Last 1 } if($ResolvedExtModuleObj) { priv_Update-CommandsToModuleMapping -ModuleInfo $ResolvedExtModuleObj -CommandSourceLocation Unknown [PSBuild.Context]::Current.ExternalModulesCache.Add([PSBuild.PSModuleBuildInfo]::new($ResolvedExtModuleObj.Name, $ResolvedExtModuleObj.Version, $null, $ResolvedExtModuleObj.Path)) } else { throw "Failed to resolve External Script Dependencies. Required module '$RequiredModuleExt' was not found." } } } } } ### PART 4: BUILD SCRIPT Write-Verbose -Message "[Build PSScript] Building Scripts..." foreach($ScriptObj in [PSBuild.Context]::Current.SolutionScriptsCache) { # PART 4-a: Update Required Module versions in the Script Manifest Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Validating Required Module Versions" $ModuleDependanciesDefinition = New-Object -TypeName System.Collections.ArrayList $UpdateScriptInfoRequired = $false foreach($requiredModule in $ScriptObj.ScriptInfo.RequiredModules) { $shouldUpdateVersion = $false $latestVersion = $null $reqModuleName = $requiredModule.Name if([PSBuild.Context]::Current.BuiltModulesCache.Contains($requiredModule.Name)) { $latestVersion = [PSBuild.Context]::Current.BuiltModulesCache.GetLatestVersion($requiredModule.Name) if(($requiredModule.Version -lt $latestVersion.Version) -and ([PSBuild.Context]::Current.UpdateModuleReferences -or [PSBuild.Context]::Current.SolutionModulesCache.Contains($requiredModule.Name))) { $shouldUpdateVersion = $true } } elseif([PSBuild.Context]::Current.ExternalModulesCache.Contains($requiredModule.Name)) { $latestVersion = [PSBuild.Context]::Current.ExternalModulesCache.GetLatestVersion($requiredModule.Name) if(-not ($latestVersion.IsPortableModule)) { $reqModuleName = $latestVersion.DestinationPath } if($requiredModule.Version -lt $latestVersion.Version) { Write-Warning -Message "Script $ScriptObj depends on $requiredModule which is not part of this solution. $($requiredModule.Name) has a newer version ($($latestVersion.Version)) and the required modules reference will be updated." $shouldUpdateVersion = $true } } else { throw "Required module $($requiredModule.Name) was not found in either the Built Modules Cache or the External Modules Cache. This should not be possible but you still did it! Congrats!" } if($shouldUpdateVersion) { $null = $ModuleDependanciesDefinition.Add([Microsoft.PowerShell.Commands.ModuleSpecification]::new(@{ ModuleName = $reqModuleName ModuleVersion = $latestVersion.Version })) Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Required Module '$($requiredModule.Name)/$($requiredModule.Version)' has a newer version ($($latestVersion.Version))" $UpdateScriptInfoRequired = $true } else { $null = $ModuleDependanciesDefinition.Add([Microsoft.PowerShell.Commands.ModuleSpecification]::new(@{ ModuleName = $reqModuleName ModuleVersion = $requiredModule.Version })) } } if ($UpdateScriptInfoRequired) { if ($UseScriptConfigFile.IsPresent) { Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Updating Script Configuration" # Config File Format $cfg = [PSCustomObject]@{ RequiredModules = @($ModuleDependanciesDefinition.ForEach{ [PSCustomObject]@{ Name = $_.Name; Version = $_.Version } }) } # Save File $cfg | ConvertTo-Json | Out-File -FilePath ([IO.Path]::Combine($ScriptObj.SourceDirectory, "$($ScriptObj.Name).config.json")) # Update Object in Memory $ScriptObj.Update($(priv_Test-Script -ScriptPath $ScriptObj.ScriptPath -UseScriptConfigFile)) } else { Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Updating Script File Info" $currentVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' Update-ScriptFileInfo -Path ($ScriptObj.ScriptPath) -RequiredModules $ModuleDependanciesDefinition -ErrorAction Stop # Update Object in Memory $ScriptObj.Update($(priv_Test-Script -ScriptPath $ScriptObj.ScriptPath)) $VerbosePreference = $currentVerbosePreference } } ### PART 4-b: Check if all commands are referenced if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.Enabled) { if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.ExcludedSources -notcontains ($ScriptObj.Name)) { # Update Script Definition. This is required in order to extract all commands used inside the script Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Reading Script AST" $scrDefiniton = priv_Get-ScriptDefinition -ScriptPath ($ScriptObj.ScriptPath) $ScriptObj.UpdateScriptDefinitionAst($scrDefiniton) Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Checking if all commands are referenced" foreach($NonLocalCommand in $ScriptObj.GetNonLocalCommands()) { if([PSBuild.Context]::Current.CheckCommandReferencesConfiguration.ExcludedCommands -notcontains $NonLocalCommand) { if([PSBuild.Context]::Current.CommandsToModuleMapping.ContainsCommand($NonLocalCommand)) { $cmdIsReferenced = $false foreach($cmdSrc in ([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand))) { if(($cmdSrc.SourceLocation -eq [PSBuild.CommandSourceLocation]::BuiltIn) -or ($ScriptObj.ScriptInfo.RequiredModules.Name -contains $cmdSrc.Source)) { # Module is either BuiltIn or explicitly referenced $cmdIsReferenced = $true break } } if(-not $cmdIsReferenced) { if([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand).Count -gt 1) { throw "Build Script '$ScriptObj' failed. The command '$NonLocalCommand' was found in modules $(@([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand).ForEach{ "'$($_.Source)'" }) -join ', ') but neither is referenced in the script metadata." } else { throw "Build Script '$ScriptObj' failed. The command '$NonLocalCommand' was found in module '$([PSBuild.Context]::Current.CommandsToModuleMapping.GetCommandSources($NonLocalCommand)[0].Source)' but it is not referenced in the script metadata." } } } else { throw "Build Script '$ScriptObj' failed. The command '$NonLocalCommand' was not found in any of the required modules referenced in the script metadata." } } } } } ### PART 4-c: Check Module Integrity if (-not $ScriptObj.IsReadyForPackaging) { throw "Build Script '$ScriptObj' failed. The script is not ready for packaging. Missing either Author or Description." } if (-not $ScriptObj.IsValid) { ## NOTE: This also updates the file hash Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Updating Script Version" $currentVerbosePreference = $VerbosePreference $VerbosePreference = 'SilentlyContinue' $oldScriptVer = $ScriptObj.ScriptInfo.Version Update-PSScriptVersion -ScriptPath ($ScriptObj.ScriptPath) -ErrorAction Stop -Verbose:$false # Update Object in Memory $ScriptObj.Update($(priv_Test-Script -ScriptPath $ScriptObj.ScriptPath -UseScriptConfigFile:($UseScriptConfigFile.IsPresent))) Write-Warning -Message "[Build PSScript] Processing $ScriptObj -> Updated script from version $($oldScriptVer) to version $($ScriptObj.ScriptInfo.Version)" $VerbosePreference = $currentVerbosePreference } ### PART 4-d: Export Script to TargetDirectory if($ScriptObj.TargetDirectory) { Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> Exporting Script" try { if( -not [IO.Directory]::Exists($ScriptObj.TargetDirectory)) { $null = [IO.Directory]::CreateDirectory($ScriptObj.TargetDirectory) } $privExportArtifact_Params = @{ SourcePath = $ScriptObj.ScriptPath DestinationPath = $ScriptObj.TargetDirectory } if ($UseScriptConfigFile.IsPresent) { $privExportArtifact_Params.Add('Type', 'ScriptWithConfig') } else { $privExportArtifact_Params.Add('Type', 'Script') } priv_Export-Artifact @privExportArtifact_Params -Verbose:$false [PSBuild.Context]::Current.BuiltScriptsCache.Add([PSBuild.PSScriptBuildInfo]::new($ScriptObj.Name, $ScriptObj.ScriptInfo.Version, $ScriptObj.ScriptPath, $ScriptObj.TargetDirectory)) } catch { throw "Build Script '$ScriptObj' failed. Cannot copy script to '$DestinationPath'. Details: $_" } } Write-Verbose -Message "[Build PSScript] Processing $ScriptObj -> DONE" } } } function Build-PSSolution { [CmdletBinding()] [OutputType([void])] param ( #SolutionConfigPath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByPath')] [System.IO.DirectoryInfo]$SolutionConfigPath, #SolutionConfig [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByObject')] [psobject]$SolutionConfigObject, #Clear [Parameter(Mandatory = $false)] [switch]$Clear ) Begin { if($Clear.IsPresent) { [PSBuild.Context]::Current.SolutionModulesCache.Clear() [PSBuild.Context]::Current.SolutionScriptsCache.Clear() [PSBuild.Context]::Current.PsGetModuleValidationCache.Clear() [PSBuild.Context]::Current.BuiltModulesCache.Clear() [PSBuild.Context]::Current.ExternalModulesCache.Clear() [PSBuild.Context]::Current.CommandsToModuleMapping.Clear() } } Process { #Initialize SolutionConfiguration try { Write-Verbose "Initialize SolutionConfiguration started" switch ($PSCmdlet.ParameterSetName) { 'NoRemoting_SolutionByPath' { $SolutionConfig = Get-PSSolutionConfiguration -Path $SolutionConfigPath.FullName -ErrorAction Stop break } 'NoRemoting_SolutionByObject' { $SolutionConfig = $SolutionConfigObject break } default { throw "Unknown ParameterSetName: $($PSCmdlet.ParameterSetName)" } } [PSBuild.Context]::Current.CheckCommandReferencesConfiguration = [PSBuild.CheckCommandReferencesConfiguration]::new($SolutionConfig.Build.CheckCommandReferences) [PSBuild.Context]::Current.AllowDuplicateCommandsInCommandToModuleMapping = (-not ($SolutionConfig.Build.CheckDuplicateCommandNames)) [PSBuild.Context]::Current.UpdateModuleReferences = $SolutionConfig.Build.UpdateModuleReferences Write-Verbose "Initialize SolutionConfiguration completed" } catch { Write-Error "Initialize SolutionConfiguration failed. Details: $_" -ErrorAction 'Stop' } #Configure PS Environment try { Write-Verbose "Configure PS Environment started" $DependancyFolders = [System.Collections.Generic.List[string]]::new() foreach ($mp in $SolutionConfig.SolutionStructure.ModulesPath) { if ($mp.ContainsKey('BuildPath')) { $DependancyFolders.Add($mp.BuildPath) } } foreach ($mp in $SolutionConfig.SolutionStructure.ScriptPath) { if ($mp.ContainsKey('DependencyDestinationPath')) { $DependancyFolders.Add($mp.DependencyDestinationPath) } elseif ($mp.ContainsKey('BuildPath')) { $DependancyFolders.Add($mp.BuildPath) } } if ($DependancyFolders.Count -gt 0) { $AddRemove_PSModulePathEntry_CommonParams = @{ path = $DependancyFolders Scope = @('User', 'Process') } #Check if AutoloadDependanciesScope are defined in config if ($SolutionConfig.Build.AutoloadDependanciesScope) { $AddRemove_PSModulePathEntry_CommonParams['Scope'] = $SolutionConfig.Build.AutoloadDependanciesScope } if ($SolutionConfig.Build.AutoloadDependancies) { Write-Verbose "Configure PS Environment in progress. Enable Module autoloading for: $($DependancyFolders -join ',')" Add-PSModulePathEntry @AddRemove_PSModulePathEntry_CommonParams -Force -ErrorAction Stop } else { Write-Verbose "Configure PS Environment in progress. Disable Module autoloading for: $($DependancyFolders -join ',')" Remove-PSModulePathEntry @AddRemove_PSModulePathEntry_CommonParams -ErrorAction Stop -WarningAction SilentlyContinue } } Write-Verbose "Configure PS Environment completed" } catch { Write-Error "Configure PS Environment failed. Details: $_" -ErrorAction 'Stop' } #Run PreBuild Actions try { Write-Verbose "Run PreBuild Actions started" Foreach ($Action in $SolutionConfig.BuildActions.PreBuild) { try { Write-Verbose "Run PreBuild Actions in progress. Action: $($Action['Name']) starting." $Null = Invoke-Command -ScriptBlock $Action['ScriptBlock'] -ErrorAction Stop -NoNewScope Write-Verbose "Run PreBuild Actions in progress. Action: $($Action['Name']) completed." } catch { Write-Warning "Run PreBuild Actions in progress. Action: $($Action['Name']) failed." throw $_ } } Write-Verbose "Run PreBuild Actions completed" } catch { Write-Error "Run PreBuild Actions failed. Details: $_" -ErrorAction 'Stop' } #Build Solution Modules try { if ($SolutionConfig.SolutionStructure.ModulesPath) { Write-Verbose "Build PSModules started" #Enumerate All Modules $ModulePathConfiguration = [PSBuild.PSModuleBuildInfoCollection]::new() foreach ($ModulePath in $SolutionConfig.SolutionStructure.ModulesPath) { $modulesFound = Get-ChildItem -Path $ModulePath.SourcePath -Directory -ErrorAction Stop -Verbose:$false foreach($moduleFound in $modulesFound) { $ModulePathConfiguration.Add([PSBuild.PSModuleBuildInfo]::new( $moduleFound.Name, $null, $moduleFound.FullName, $ModulePath.BuildPath )) } } if ($ModulePathConfiguration.Count -gt 0) { $BuildPSModule_Params = @{ ModulePathConfiguration = $ModulePathConfiguration ResolveDependancies = $SolutionConfig.Build.AutoResolveDependantModules PSGetRepository = $SolutionConfig.Packaging.PSGetSearchRepositories } if ($SolutionConfig.GlobalSettings.Proxy.Uri) { $BuildPSModule_Params.Add('Proxy', $SolutionConfig.GlobalSettings.Proxy.Uri) } Build-PSModule @BuildPSModule_Params -ErrorAction Stop } Write-Verbose "Build PSModules completed" } else { Write-Verbose "Build PSModules skipped" } } catch { Write-Error "Build Solution Modules failed. Details: $_" -ErrorAction 'Stop' } #Build Solution Scripts try { Write-Verbose "Build Solution Scripts started" #Enumerate All Scripts $ScriptPathConfiguration = [PSBuild.PSScriptBuildInfoCollection]::new() foreach ($ScriptPath in $SolutionConfig.SolutionStructure.ScriptPath) { if(($SolutionConfig.Build.AutoResolveDependantModules) -and [String]::IsNullOrEmpty($ScriptPath.BuildPath) -and [String]::IsNullOrEmpty($ScriptPath.DependencyDestinationPath)) { throw "Build Solution Scripts failed. The Solution Configuration is invalid. ScriptPath requires you to specify at least one of the following properties: BuildPath, DependencyDestinationPath." } $scriptsFound = Get-ChildItem -Path $ScriptPath.SourcePath -Filter *.ps1 -ErrorAction Stop foreach($scriptFound in $scriptsFound) { $ScriptPathConfiguration.Add([PSBuild.PSScriptBuildInfo]::new(@{ Name = $scriptFound.Name SourcePath = $scriptFound.FullName DestinationPath = $ScriptPath.BuildPath RequiredModulesDestinationPath = $(if($ScriptPath.DependencyDestinationPath) {$ScriptPath.DependencyDestinationPath} else {$ScriptPath.BuildPath}) })) } } if ($ScriptPathConfiguration.Count -gt 0) { $BuildPSScript_Params = @{ ScriptPathConfiguration = $ScriptPathConfiguration ResolveDependancies = $SolutionConfig.Build.AutoResolveDependantModules UseScriptConfigFile = $SolutionConfig.Build.UseScriptConfigFile PSGetRepository = $SolutionConfig.Packaging.PSGetSearchRepositories } if ($SolutionConfig.GlobalSettings.Proxy.Uri) { $BuildPSScript_Params.Add('Proxy', $SolutionConfig.GlobalSettings.Proxy.Uri) } Build-PSScript @BuildPSScript_Params -ErrorAction Stop } Write-Verbose "Build Solution Scripts completed" } catch { Write-Error "Build Solution Scripts failed. Details: $_" -ErrorAction 'Stop' } #Run PostBuild Actions try { Write-Verbose "Run PostBuild Actions started" Foreach ($Action in $SolutionConfig.BuildActions.PostBuild) { try { Write-Verbose "Run PostBuild Actions in progress. Action: $($Action['Name']) starting." $Null = Invoke-Command -ScriptBlock $Action['ScriptBlock'] -ErrorAction Stop -NoNewScope Write-Verbose "Run PostBuild Actions in progress. Action: $($Action['Name']) completed." } catch { Write-Warning "Run PostBuild Actions in progress. Action: $($Action['Name']) failed." throw $_ } } Write-Verbose "Run PostBuild Actions completed" } catch { Write-Error "Run PostBuild Actions failed. Details: $_" -ErrorAction 'Stop' } } } function Publish-PSSolution { [CmdletBinding()] [OutputType([void])] param ( #SolutionConfigPath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByPath')] [System.IO.DirectoryInfo]$SolutionConfigPath, #SolutionConfig [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByObject')] [psobject]$SolutionConfigObject ) Process { #Initialize SolutionConfiguration try { Write-Verbose "Initialize SolutionConfiguration started" switch ($PSCmdlet.ParameterSetName) { 'NoRemoting_SolutionByPath' { $SolutionConfig = Get-PSSolutionConfiguration -Path $SolutionConfigPath.FullName -ErrorAction Stop break } 'NoRemoting_SolutionByObject' { $SolutionConfig = $SolutionConfigObject break } default { throw "Unknown ParameterSetName: $($PSCmdlet.ParameterSetName)" } } Write-Verbose "Initialize SolutionConfiguration completed" } catch { Write-Error "Initialize SolutionConfiguration failed. Details: $_" -ErrorAction 'Stop' } #Publish Solution Modules try { Write-Verbose "Publish Solution Modules started" #Get All Modules $Modules = New-Object -TypeName system.collections.arraylist foreach ($modPath in $SolutionConfig.SolutionStructure.ModulesPath) { Get-ChildItem -Path $modPath.SourcePath -Directory -ErrorAction Stop | ForEach-Object { $ModuleToPathMapping = @{ Module = $_ SourcePath = Join-Path -Path $modPath.SourcePath -ChildPath $_.Name BuildPath = Join-Path -Path $modPath.BuildPath -ChildPath $_.Name } $null = $Modules.Add($ModuleToPathMapping) } } #Determine which modules should be published if ((-not $SolutionConfig.Packaging.PublishAllModules) -and ($SolutionConfig.Packaging.PublishSpecificModules.Count -gt 0)) { $Modules = $Modules | Where-Object { $SolutionConfig.Packaging.PublishSpecificModules -contains $_.Module.Name } } elseif ($SolutionConfig.Packaging.PublishAllModules -and ($SolutionConfig.Packaging.PublishSpecificModules.Count -gt 0)) { Write-Warning "Publish Solution Modules in progress. No Modules are configured to be published" } if ($SolutionConfig.Packaging.PublishExcludeModules.Count -gt 0) { $Modules = $Modules | Where-Object { $SolutionConfig.Packaging.PublishExcludeModules -ne $_.Module.Name } } #Determine if there are PSGetRepositories specified for publishing if ($Modules) { if ($SolutionConfig.Packaging.PSGetPublishRepositories.Count -eq 0) { Write-Warning "Publish Solution Modules in progress. There are modules for publishing, but no PSGetPublishRepositories are specified" } } else { Write-Warning "Publish Solution Modules in progress. No Modules for publishing" } #Install Nuget PackageProvider try { $NuGetProvider = Get-PackageProvider -Name nuget -ErrorAction Stop | Where-Object { [version]$_.Version -ge [version]'2.8.5.208' } } catch { } if (-not $NuGetProvider) { $null = Install-PackageProvider -Name Nuget -Force -Confirm:$false -Verbose:$false -Scope CurrentUser } #Publish Modules to each PSGetPublishRepositories foreach ($Repo in $SolutionConfig.Packaging.PSGetPublishRepositories) { Add-PSModulePathEntry -Scope Process -Path $Modules.BuildPath -ErrorAction Stop Publish-PSModule -ModulePath $Modules.BuildPath -PSGetRepository $Repo -ErrorAction Stop } Write-Verbose "Publish Solution Modules completed" } catch { Write-Error "Publish Solution Modules failed. Details: $_" -ErrorAction 'Stop' } } } function Publish-PSModule { [CmdletBinding()] param ( #ModulesFolder [Parameter(Mandatory = $true)] [System.IO.DirectoryInfo[]]$ModulePath, #PSGetRepository [Parameter(Mandatory = $false)] [hashtable]$PSGetRepository, #SkipVersionValidation [Parameter(Mandatory = $false)] [switch]$SkipVersionValidation, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { #Check if Repository is already registered try { Write-Verbose "Check if Repository is already registered started" $AssertPSRepository_Params = @{ PSGetRepository = $PSGetRepository } if ($PSBoundParameters.ContainsKey('Proxy')) { $AssertPSRepository_Params.Add('Proxy', $Proxy) } Assert-PSRepository @AssertPSRepository_Params -ErrorAction Stop Write-Verbose "Check if Repository is already registered completed" } catch { Write-Error "Check if Repository is already registered failed. Details: $_" -ErrorAction 'Stop' } #Publish Module foreach ($Module in $ModulePath) { try { $moduleName = $Module.Name Write-Verbose "Publish Module:$moduleName started" #Check if Module is already built $ModInfo = Test-PSModule -ModulePath $Module -ErrorAction Stop if (-not $ModInfo.IsVersionValid -and (-not $SkipVersionValidation.IsPresent)) { throw 'not builded' } #Publish Module $PublishModuleAndDependacies_Params = @{ ModuleInfo = $ModInfo.ModuleInfo Repository = $PSGetRepository.Name PublishDependantModules = $true Force = $true } if ($PSGetRepository.ContainsKey('Credential')) { $PublishModuleAndDependacies_Params.Add('Credential', $PSGetRepository.Credential) } if ($PSGetRepository.ContainsKey('NuGetApiKey')) { $PublishModuleAndDependacies_Params.Add('NuGetApiKey', $PSGetRepository.NuGetApiKey) } if ($PSBoundParameters.ContainsKey('Proxy')) { $PublishModuleAndDependacies_Params.Add('Proxy', $Proxy) } priv_Publish-PSModule @PublishModuleAndDependacies_Params -ErrorAction Stop -Verbose -VerbosePrefix "Publish Module:$moduleName in progress. " Write-Verbose "Publish Module:$moduleName completed" } catch { Write-Error "Publish Module:$moduleName failed. Details: $_" -ErrorAction Stop } } } } function Get-PSSolutionConfiguration { [CmdletBinding()] [OutputType([void])] param ( #Path [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_Default')] [System.IO.FileInfo]$Path, #UserVariables [Parameter(Mandatory = $false, ParameterSetName = 'NoRemoting_Default')] [hashtable]$UserVariables ) Process { try { $SolutionConfig = [System.Collections.Generic.List[string]]::new() #Add Variables $SolutionConfig.Add("`$env:ScriptRoot = '$(Split-Path -Path $Path.FullName -Parent -ErrorAction Stop)'") if ($PSBoundParameters.ContainsKey('UserVariables')) { $SolutionConfig.Add("`$Variables = $(Convertto-string -InputObject $UserVariables)") } else { $SolutionConfig.Add("`$Variables = @{}") } $SolutionConfig.Add("`$Variables['ScriptRoot'] = '$(Split-Path -Path $Path.FullName -Parent -ErrorAction Stop)'") #Get config from file Get-Content -Path $Path.FullName -ErrorAction Stop | foreach { $SolutionConfig.Add($_) } $SolutionConfigAsString = $SolutionConfig -join [System.Environment]::NewLine New-DynamicConfiguration -Definition ([scriptblock]::Create($SolutionConfigAsString)) -ErrorAction Stop } catch { Write-Error "Unable to load SolutionConfiguration: $($Path.FullName). Details: $_" } } } function Clear-PSSolution { [CmdletBinding()] [OutputType([void])] param ( #SolutionConfigPath [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByPath')] [System.IO.DirectoryInfo]$SolutionConfigPath, #SolutionConfig [Parameter(Mandatory = $true, ParameterSetName = 'NoRemoting_SolutionByObject')] [psobject]$SolutionConfigObject ) Process { #Initialize SolutionConfiguration try { Write-Verbose "Initialize SolutionConfiguration started" switch ($PSCmdlet.ParameterSetName) { 'NoRemoting_SolutionByPath' { $SolutionConfig = Get-PSSolutionConfiguration -Path $SolutionConfigPath.FullName -ErrorAction Stop break } 'NoRemoting_SolutionByObject' { $SolutionConfig = $SolutionConfigObject break } default { throw "Unknown ParameterSetName: $($PSCmdlet.ParameterSetName)" } } Write-Verbose "Initialize SolutionConfiguration completed" } catch { Write-Error "Initialize SolutionConfiguration failed. Details: $_" -ErrorAction 'Stop' } #Clear Modules try { Write-Verbose "Clear Modules started" foreach ($Entry in $SolutionConfig.SolutionStructure.ModulesPath) { if ($Entry.BuildPath -and (Test-Path -Path $Entry.BuildPath)) { Write-Verbose "Clear Modules in proress. Deleting PSModuleBuildPath: $($Entry.BuildPath)" Remove-Item -Path $Entry.BuildPath -Force -Recurse -ErrorAction Stop } } Write-Verbose "Clear Modules completed" } catch { Write-Error "Clear Modules failed. Details: $_" -ErrorAction 'Stop' } #Clear Scripts try { Write-Verbose "Clear Scripts started" foreach ($Entry in $SolutionConfig.SolutionStructure.ScriptPath) { if ($Entry.BuildPath -and (Test-Path -Path $Entry.BuildPath)) { Write-Verbose "Clear Scripts in proress. Deleting PSScriptBuildPath: $($Entry.BuildPath)" Remove-Item -Path $Entry.BuildPath -Force -Recurse -ErrorAction Stop } if ($Entry.DependencyDestinationPath -and (Test-Path -Path $Entry.DependencyDestinationPath)) { Write-Verbose "Clear Scripts in proress. Deleting PSScriptDependencyDestinationPath: $($Entry.DependencyDestinationPath)" Remove-Item -Path $Entry.DependencyDestinationPath -Force -Recurse -ErrorAction Stop } } Write-Verbose "Clear Scripts completed" } catch { Write-Error "Clear Scripts failed. Details: $_" -ErrorAction 'Stop' } } } function Assert-PSRepository { [CmdletBinding()] [OutputType([void])] param ( #PSGetRepository [Parameter(Mandatory = $true)] [hashtable[]]$PSGetRepository, #Proxy [Parameter(Mandatory = $false)] [uri]$Proxy ) Process { foreach ($Repo in $PSGetRepository) { if([PSBuild.Context]::Current.AssertedPSRepositories -notcontains ($Repo.Name)) # local cache to speed things up with multiple calls { #Check if Repository is already registered $RepoFound = $false try { $VerbosePreference = 'SilentlyContinue' $RepoCheck = Get-PSRepository -Name $Repo.Name -ErrorAction Stop -Verbose:$false if ($RepoCheck) { if ($Repo.ContainsKey('SourceLocation') -and ($RepoCheck.SourceLocation -ne $Repo.SourceLocation)) { $SetPSRepository_Params = @{ Name = $Repo.Name SourceLocation = $Repo.SourceLocation } if ($PSBoundParameters.ContainsKey('Proxy')) { $SetPSRepository_Params.Add('Proxy', $Proxy) } Set-PSRepository @SetPSRepository_Params -ErrorAction Stop -Verbose:$false } if ($Repo.ContainsKey('PublishLocation') -and ($RepoCheck.PublishLocation -ne $Repo.PublishLocation)) { $SetPSRepository_Params = @{ Name = $Repo.Name SourceLocation = $Repo.PublishLocation } if ($PSBoundParameters.ContainsKey('Proxy')) { $SetPSRepository_Params.Add('Proxy', $Proxy) } Set-PSRepository @SetPSRepository_Params -ErrorAction Stop -Verbose:$false } $RepoFound = $true } } catch { } if (-not $RepoFound) { $RegisterPSRepository_Params = @{ } + $Repo if ($PSBoundParameters.ContainsKey('Proxy')) { $RegisterPSRepository_Params.Add('Proxy', $Proxy) } $null = Register-PSRepository @RegisterPSRepository_Params -ErrorAction Stop -Verbose:$false } [PSBuild.Context]::Current.AssertedPSRepositories.Add($Repo.Name) } } } } #endregion #region private Variables $PSNativeModules = @( 'Microsoft.PowerShell.Management' 'Microsoft.PowerShell.Core' 'Microsoft.PowerShell.Utility' 'Microsoft.PowerShell.Security' 'Microsoft.PowerShell.Archive' ) #endregion |