DescriptionFile.ps1
$nameRegex = "^[0-9A-Za-z_]+$" $versionRegex = "^v?(?:(?:(?<epoch>[0-9]+)!)?(?<release>[0-9]*(?:[_\.][0-9]+)*)(?<pre>[_\.]?(?<pre_l>(a|b|c|rc|alpha|beta|pre|preview|sp))[_\.]?(?<pre_n>[0-9]+)?)?(?<post>(?:-(?<post_n1>[0-9]+))|(?:[_\.]?(?<post_l>post|rev|r)[_\.]?(?<post_n2>[0-9]+)?))?(?<dev>[_\.]?(?<dev_l>dev)[_\.]?(?<dev_n>[0-9]+)?)?)(?:\+(?<local>[a-z0-9]+(?:[_\.][a-z0-9]+)*))?$" $architectureRegex = "^x64|x86$" $additionalOptionsRegex = "^[0-9A-Za-z.]+$" function Split-EnvironmentModuleName([String] $ModuleFullName, [switch] $Silent) { <# .SYNOPSIS Splits the given name into an array with 4 parts (name, version, architecture, additionalOptions). .DESCRIPTION Split a name string that either has the format 'Name-Version-Architecture' or just 'Name'. The output is an anonymous object with the 4 properties (name, version, architecture, additionalOptions). If a value was not specified, $null is returned at the according array index. .PARAMETER ModuleFullName The full name of the module that should be splitted. .OUTPUTS A string array with 4 parts (name, version, architecture, additionalOptions) #> $parts = $ModuleFullName.Split("-") $nameMatchResult = [System.Text.RegularExpressions.Regex]::Match($parts[0], $nameRegex, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $result = @{} $result.Name = $nameMatchResult.Value $regexOrder = @(@($versionRegex, "Version"), @($architectureRegex, "Architecture"), @($additionalOptionsRegex, "AdditionalOptions")) $currentRegexIndex = 0 $matchFailed = (-not ($nameMatchResult.Success)) for($i = 1; $i -lt $parts.Count; $i++) { if($currentRegexIndex -ge $regexOrder.Count) { # More parts than matching regexes found $matchFailed = $true break } $currentRegex = $regexOrder[$currentRegexIndex][0] $matchResult = [System.Text.RegularExpressions.Regex]::Match($parts[$i], $currentRegex, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) if($matchResult.Success) { $result.($regexOrder[$currentRegexIndex][1]) = $matchResult.Value } else { $i-- # We have to check the same part again with the next regex in the list } $currentRegexIndex++ } if($matchFailed) { if(-not ($Silent)) { Write-Warning "The environment module name '$ModuleFullName' is not correctly formated. It must be 'Name-Version-Architecture-AdditionalOptions'" } return $null } return $result } function Read-EnvironmentModuleDescriptionFile([string] $ModuleBase, [string] $ModuleFullName) { <# .SYNOPSIS Read the Environment Module file (*.pse) of the of the given module. .DESCRIPTION This function will read the environment module info of the given module. If the module does not depend on the environment module, $null is returned. If no description file was found, an empty map is returned. .OUTPUTS The map containing the values or $null. #> Write-Verbose "Reading environment module description file for $($Module.Name)" # Search for a pse1 file in the base directory return Read-EnvironmentModuleDescriptionFileByPath (Join-Path $ModuleBase "$($ModuleFullName).pse1") } function Read-EnvironmentModuleDescriptionFileByPath([string] $Path) { <# .SYNOPSIS Read the given Environment Module file (*.pse). .DESCRIPTION This function will read the environment module info. If the description file was not found, an empty map is returned. .OUTPUTS The map containing the values or $null. #> if(Test-Path $Path) { # Parse the pse1 file Write-Verbose "Found desciption file $descriptionFile" return Import-PowershellDataFile $Path } return @{} } function New-EnvironmentModuleInfoBase { <# .SYNOPSIS Create a new EnvironmentModuleInfoBase object from the given parameters. .PARAMETER Module The module info that contains the base information. .OUTPUTS The created object of type EnvironmentModuleInfoBase or $null. .NOTES The given module name must match exactly one module, otherwise $null is returned. #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param ( [PSModuleInfo] $Module ) $nameParts = Split-EnvironmentModuleName $Module.Name if($null -eq $nameParts) { return $null } $descriptionContent = Read-EnvironmentModuleDescriptionFile $Module.ModuleBase $Module.Name if(-not $descriptionContent) { return $null } $result = New-Object EnvironmentModuleCore.EnvironmentModuleInfoBase -ArgumentList @($Module.Name, $Module.ModuleBase, $nameParts.Name, $nameParts.Version, $nameParts.Architecture, $nameParts.AdditionalOptions, [EnvironmentModuleCore.EnvironmentModuleType]::Default) Set-EnvironmentModuleInfoBaseParameter $result $descriptionContent return $result } function Set-EnvironmentModuleInfoBaseParameter { <# .SYNOPSIS Assign the given parameters to the passed module object. .PARAMETER Module The module to modify. .PARAMETER Parameters The parameters to set. #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param( [EnvironmentModuleCore.EnvironmentModuleInfoBase][ref] $Module, [hashtable] $Parameters ) if($Parameters.Contains("ModuleType")) { $Module.ModuleType = [Enum]::Parse([EnvironmentModuleCore.EnvironmentModuleType], $descriptionContent.Item("ModuleType")) Write-Verbose "Read module type $($Module.ModuleType)" } } function New-EnvironmentModuleInfo { <# .SYNOPSIS Create a new EnvironmentModuleInfo object from the given parameters. .PARAMETER Module The module info that contains the base information. .PARAMETER ModuleFullName The full name of the module. Only used if the module parameter is not set. .PARAMETER ModuleFile The module file (psd1) to load. If this is set, the ModuleFullName is not evaluated. .OUTPUTS The created object of type EnvironmentModuleInfo or $null. .NOTES The given module name must match exactly one module, otherwise $null is returned. #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param ( [EnvironmentModuleCore.EnvironmentModuleInfoBase] $Module = $null, [String] $ModuleFullName = $null, [String] $ModuleFile = $null ) if($null -eq $Module) { if(-not ([string]::IsNullOrEmpty($ModuleFile))) { $matchingModules = (Get-Module "$ModuleFile" -ListAvailable) if($matchingModules.Length -lt 1) { Write-Verbose "Unable to find the module $ModuleFile" return $null } $Module = New-EnvironmentModuleInfoBase $matchingModules[0] } else { $matchingModules = Get-EnvironmentModule -ListAvailable $ModuleFullName if($matchingModules.Length -lt 1) { Write-Verbose "Unable to find the module $ModuleFullName in the list of all environment modules" return $null } if($matchingModules.Length -gt 1) { Write-Warning "More than one environment module matches the given full name '$ModuleFullName'" } $Module = $matchingModules[0] } } $descriptionContent = Read-EnvironmentModuleDescriptionFile $Module.ModuleBase $Module.FullName if(-not $descriptionContent) { return $null } $arguments = @($Module, $null, (Join-Path $script:tmpEnvironmentRootSessionPath $Module.Name)) $result = New-Object EnvironmentModuleCore.EnvironmentModuleInfo -ArgumentList $arguments Set-EnvironmentModuleInfoBaseParameter $result $descriptionContent $result.DirectUnload = $false $customSearchPaths = $script:customSearchPaths[$Module.FullName] if ($customSearchPaths) { $result.SearchPaths = $result.SearchPaths + $customSearchPaths } $dependencies = @() if($descriptionContent.Contains("RequiredEnvironmentModules")) { Write-Warning "The field 'RequiredEnvironmentModules' defined for '$($Module.FullName)' is deprecated, please use the dependencies field." $dependencies = $descriptionContent.Item("RequiredEnvironmentModules") | Foreach-Object { New-Object "EnvironmentModuleCore.DependencyInfo" -ArgumentList $_} Write-Verbose "Read module dependencies $($dependencies)" } if($descriptionContent.Contains("Dependencies")) { $dependencies = $dependencies + ($descriptionContent.Item("Dependencies") | Foreach-Object { if($_.GetType() -eq [string]) { New-Object "EnvironmentModuleCore.DependencyInfo" -ArgumentList $_ } else { New-Object "EnvironmentModuleCore.DependencyInfo" -ArgumentList $_.Name, $_.Optional } }) Write-Verbose "Read module dependencies $($dependencies)" } $result.Dependencies = $dependencies if($descriptionContent.Contains("DirectUnload")) { $result.DirectUnload = $descriptionContent.Item("DirectUnload") Write-Verbose "Read module direct unload $($result.DirectUnload)" } $requiredItems = @() if($descriptionContent.Contains("RequiredFiles")) { Write-Warning "The field 'RequiredFiles' defined for '$($Module.FullName)' is deprecated, please use the RequiredItems field." $requiredItems = $result.RequiredItems + ($descriptionContent.Item("RequiredFiles") | ForEach-Object { New-Object "EnvironmentModuleCore.RequiredItem" -ArgumentList ([EnvironmentModuleCore.RequiredItem]::TYPE_FILE), $_ }) Write-Verbose "Read required files $($descriptionContent.Item('RequiredFiles'))" } if($descriptionContent.Contains("RequiredItems") -and $descriptionContent.Item("RequiredItems").count -gt 0) { $requiredItems = $requiredItems + ($descriptionContent.Item("RequiredItems") | Foreach-Object { if($_.GetType() -eq [string]) { New-Object "EnvironmentModuleCore.RequiredItem" -ArgumentList ([EnvironmentModuleCore.RequiredItem]::TYPE_FILE), $_ } else { New-Object "EnvironmentModuleCore.RequiredItem" -ArgumentList $_.Type, $_.Value } }) Write-Verbose "Read module dependencies $($dependencies)" } $result.RequiredItems = $requiredItems if($descriptionContent.Contains("DefaultRegistryPaths") -and $descriptionContent.Item("DefaultRegistryPaths").count -gt 0) { Write-Warning "The field 'DefaultRegistryPaths' defined for '$($Module.FullName)' is deprecated, please use the DefaultSearchPaths field." $pathValues = $descriptionContent.Item("DefaultRegistryPaths") $searchPathType = "REGISTRY" $searchPathPriority = $script:searchPathTypes[$searchPathType].Item2 Write-Verbose "Read default registry paths $($result.DefaultRegistryPaths)" $result.SearchPaths = $result.SearchPaths + ($pathValues | ForEach-Object { $parts = $_.Split([IO.Path]::PathSeparator) + @("") New-Object "EnvironmentModuleCore.SearchPath" -ArgumentList @($parts[0], $searchPathType, $searchPathPriority, $parts[1], $true) }) } if($descriptionContent.Contains("DefaultFolderPaths") -and $descriptionContent.Item("DefaultFolderPaths").count -gt 0) { Write-Warning "The field 'DefaultFolderPaths' defined for '$($Module.FullName)' is deprecated, please use the DefaultSearchPaths field." $pathValues = $descriptionContent.Item("DefaultFolderPaths") $searchPathType = [EnvironmentModuleCore.SearchPath]::TYPE_DIRECTORY $searchPathPriority = $script:searchPathTypes[$searchPathType].Item2 Write-Verbose "Read default folder paths $($result.DefaultFolderPaths)" $result.SearchPaths = $result.SearchPaths + ($pathValues | ForEach-Object { $parts = $_.Split([IO.Path]::PathSeparator) + @("") New-Object "EnvironmentModuleCore.SearchPath" -ArgumentList @($parts[0], $searchPathType, $searchPathPriority, $parts[1], $true) }) } if($descriptionContent.Contains("DefaultEnvironmentPaths") -and $descriptionContent.Item("DefaultEnvironmentPaths").count -gt 0) { Write-Warning "The field 'DefaultEnvironmentPaths' defined for '$($Module.FullName)' is deprecated, please use the DefaultSearchPaths field." $pathValues = $descriptionContent.Item("DefaultEnvironmentPaths") $searchPathType = [EnvironmentModuleCore.SearchPath]::TYPE_ENVIRONMENT_VARIABLE $searchPathPriority = $script:searchPathTypes[$searchPathType].Item2 Write-Verbose "Read default environment paths $($result.DefaultEnvironmentPaths)" $result.SearchPaths = $result.SearchPaths + ($pathValues | ForEach-Object { $parts = $_.Split([IO.Path]::PathSeparator) + @("") New-Object "EnvironmentModuleCore.SearchPath" -ArgumentList @($parts[0], $searchPathType, $searchPathPriority, $parts[1], $true) }) } if($descriptionContent.Contains("DefaultSearchPaths") -and $descriptionContent.Item("DefaultSearchPaths").count -gt 0) { $result.SearchPaths = $result.SearchPaths + ($descriptionContent.Item("DefaultSearchPaths") | ForEach-Object { if($_.GetType() -eq [string]) { $searchPathType = [EnvironmentModuleCore.SearchPath]::TYPE_DIRECTORY $searchPathPriority = $script:searchPathTypes[$searchPathType].Item2 New-Object "EnvironmentModuleCore.SearchPath" -ArgumentList $_, $searchPathType, $searchPathPriority, $null, $true } else { $searchPathType = $_.Type $searchPathPriority = $_.Priority if($null -eq $searchPathPriority) { $searchPathPriority = $script:searchPathTypes[$searchPathType].Item2 } New-Object "EnvironmentModuleCore.SearchPath" -ArgumentList $_.Key, $searchPathType, $searchPathPriority, $_.SubFolder, $true } }) Write-Verbose "Read module default search paths $($result.SearchPaths)" } if($descriptionContent.Contains("StyleVersion")) { $result.StyleVersion = $descriptionContent.Item("StyleVersion") Write-Verbose "Read module style version $($result.StyleVersion)" } if($descriptionContent.Contains("Category")) { $result.Category = $descriptionContent.Item("Category") Write-Verbose "Read module category $($result.Category)" } if($descriptionContent.Contains("Parameters")) { $parameters = $descriptionContent.Item("Parameters") if($parameters -is [array]) { # Handle the complex syntax $parameters | Foreach-Object { $virtualEnvironment = $_.VirtualEnvironment if([string]::IsNullOrEmpty($virtualEnvironment)) { $virtualEnvironment = "Default" } $parameterKey = [System.Tuple[string, string]]::new($_.Name, $virtualEnvironment) $result.Parameters[$parameterKey] = (New-Object "EnvironmentModuleCore.ParameterInfoBase" -ArgumentList $_.Name, $_.Value, $_.IsUserDefined, $virtualEnvironment) } } else { # Handle the simple syntax $parameters.Keys | Foreach-Object { $virtualEnvironment = "Default" $parameterKey = [System.Tuple[string, string]]::new($_, $virtualEnvironment) $result.Parameters[$parameterKey] = (New-Object "EnvironmentModuleCore.ParameterInfoBase" -ArgumentList $_, $parameters[$_], $false, $virtualEnvironment) } } Write-Verbose "Read module parameters $($result.Parameters.GetEnumerator() -join ',')" } if($descriptionContent.Contains("Paths")) { $descriptionContent.Item("Paths") | Foreach-Object { $mode = [EnvironmentModuleCore.PathType]::UNKNOWN [Enum]::TryParse($_.Mode, [ref] $mode) | Out-Null if([String]::IsNullOrEmpty($_.Variable)) { Write-Error "Path definition without 'Variable' defined in module definition $($Module.FullName)" return } $pathInfo = $null $pathDefinition = $_ $value = Expand-PathSeparators $pathDefinition.Value switch ($mode) { APPEND { $pathInfo = $result.AddAppendPath($pathDefinition.Variable, $value, $pathDefinition.Key) } PREPEND { $pathInfo = $result.AddPrependPath($pathDefinition.Variable, $value, $pathDefinition.Key) } SET { $pathInfo = $result.AddSetPath($pathDefinition.Variable, $value, $pathDefinition.Key) } Default { Write-Error "Unable to handle of Mode of static path definition of module $($Module.FullName)" return } } Write-Verbose "Added path definition: $($pathInfo.ToString())" } } return $result } function Compare-EnvironmentModulesByVersion([EnvironmentModuleCore.EnvironmentModuleInfoBase[]] $EnvironmentModules) { <# .SYNOPSIS Compare the given environment modules by its version. If the version is equal, the architecture is compared. .PARAMETER EnvironmentModules The environment modules to compare. .OUTPUTS The sorted environment modules. #> if($null -eq $EnvironmentModules) { return $null } $versionMatches = [System.Collections.Generic.Dictionary[String, System.Text.RegularExpressions.Match]]::new() foreach($environmentModule in $EnvironmentModules) { if([String]::IsNullOrEmpty($environmentModule.Version)) { $versionMatches.Add($environmentModule.FullName, $null) continue } $versionMatch = [System.Text.RegularExpressions.Regex]::Match($environmentModule.Version, $versionRegex, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) if($versionMatch.Success) { $versionMatches.Add($environmentModule.FullName, $versionMatch) continue } $versionMatches.Add($environmentModule.FullName, $null) } $moduleList = [System.Collections.Generic.List[EnvironmentModuleCore.EnvironmentModuleInfoBase]]::new($EnvironmentModules) class EnvironmentModuleComparator : System.Collections.Generic.IComparer[EnvironmentModuleCore.EnvironmentModuleInfoBase] { [System.Collections.Generic.Dictionary[String, System.Text.RegularExpressions.Match]] $versionMatches EnvironmentModuleComparator([System.Collections.Generic.Dictionary[String, System.Text.RegularExpressions.Match]] $versionMatches) { $this.versionMatches = $versionMatches } [int] Compare([EnvironmentModuleCore.EnvironmentModuleInfoBase] $a, [EnvironmentModuleCore.EnvironmentModuleInfoBase] $b) { $matchA = $this.versionMatches[$a.FullName] $matchB = $this.versionMatches[$b.FullName] if($null -eq $matchA) { if($null -eq $matchB) { return $a.Architecture.CompareTo($b.Architecture) } return 1 } if($null -eq $matchB){ return -1 } $versionPartsA = $matchA.Groups[0].Value.Replace("_", ".").Split(".") $versionPartsB = $matchB.Groups[0].Value.Replace("_", ".").Split(".") for($i = 0; $i -lt $versionPartsA.Length; $i++) { # the Version A has more parts than B -> A wins if($i -gt ($versionPartsB.Length - 1)) { return -1 } $partA = $versionPartsA[$i] $partB = $versionPartsB[$i] try { [int]::TryParse($partA, [ref] $partA) | Out-Null } catch { } try { [int]::TryParse($partB, [ref] $partB) | Out-Null } catch { } if($partA -gt $partB) { return -1 } if($partB -gt $partA) { return 1 } } if($versionPartsB.Length -gt $versionPartsA.Length) { # The Version B has more parts than A -> B wins return 1 } return $a.Architecture.CompareTo($b.Architecture) } } $comparator = [EnvironmentModuleComparator]::new($versionMatches) $moduleList.Sort($comparator); return $moduleList } |