functions/settings.ps1
|
$script:ConfigMapSettings = $null # Defaults plus QCONF_* environment variables, captured at import or by Update-ConfigMapSettings. # Entry settings are resolved from this base and the map, never from the caller's settings. $script:ConfigMapBaseSettings = $null function New-ConfigMapSettings { param( $BaseSettings, [System.Collections.IDictionary]$Overrides = @{} ) $settings = @{ Debug = $false Concurrently = $false TmuxAutoWindow = $false DiscoveryCache = $true DiscoveryCacheLocation = 'local' Process = $script:ConfigMapDefaultProcessName ForceCheck = $false UseWorkDir = $false } foreach ($propertyName in @($settings.Keys)) { $envPath = "env:QCONF_$propertyName" if (Test-Path -Path $envPath) { $settings[$propertyName] = (Get-Item -Path $envPath).Value } } if ($BaseSettings) { foreach ($propertyName in @($settings.Keys)) { $settings[$propertyName] = $BaseSettings.PSObject.Properties[$propertyName].Value } } foreach ($override in $Overrides.GetEnumerator()) { if (-not $settings.ContainsKey($override.Key)) { throw "Unknown ConfigMap setting '$($override.Key)'." } $settings[$override.Key] = $override.Value } $normalizedLocation = "$($settings.DiscoveryCacheLocation)".Trim().ToLowerInvariant() if ($normalizedLocation -notin @('local', 'user')) { throw "DiscoveryCacheLocation must be 'local' or 'user'." } $settings.DiscoveryCacheLocation = $normalizedLocation $settings.Process = Get-ConfigMapProcessCanonicalName -ProcessName "$($settings.Process)" $settingsObject = [pscustomobject]$settings $settingsObject.PSObject.TypeNames.Insert(0, 'ConfigMap.Settings') return $settingsObject } function Update-ConfigMapSettings { $script:ConfigMapBaseSettings = New-ConfigMapSettings $script:ConfigMapSettings = $script:ConfigMapBaseSettings return $script:ConfigMapSettings } function Enter-ConfigMapSettingsScope { param( [System.Collections.IDictionary]$Settings ) $previousSettings = $script:ConfigMapSettings if ($Settings) { $script:ConfigMapSettings = New-ConfigMapSettings -BaseSettings $previousSettings -Overrides $Settings } return $previousSettings } function Exit-ConfigMapSettingsScope { param($PreviousSettings) $script:ConfigMapSettings = $PreviousSettings } function New-ConfigMapOperationContext { return @{ IncludeCache = @{} LoadingIncludes = @{} Dependencies = @{} } } function Find-ConfigMapAncestorSettings { param( [System.Collections.IDictionary]$Node, [string[]]$Segments, [switch]$IncludeTarget, [System.Collections.Generic.HashSet[string]]$Visited, [hashtable]$Cache, [hashtable]$Loading ) $notFound = [pscustomobject]@{ Found = $false; Settings = $null } if ($null -eq $Segments -or $Segments.Count -eq 0 -or $null -eq $Node) { return $notFound } $segment = $Segments[0] $rest = if ($Segments.Count -gt 1) { $Segments[1..($Segments.Count - 1)] } else { @() } $isLast = $rest.Count -eq 0 $visitKey = '{0}|{1}' -f [System.Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($Node), $segment if (-not $Visited.Add($visitKey)) { return $notFound } $baseDir = $Node._baseDir $list = $Node if ($Node.list) { $list = $Node.list } if ($list -isnot [System.Collections.IDictionary]) { return $notFound } if ($list.Contains($segment)) { $settings = [System.Collections.Generic.List[object]]::new() $child = $list[$segment] $apply = (-not $isLast) -or $IncludeTarget if ($apply -and $child -is [System.Collections.IDictionary] -and $child._settings) { $settings.Add($child._settings) } if (-not $isLast -and $child -is [System.Collections.IDictionary]) { $inner = Find-ConfigMapAncestorSettings -Node $child -Segments $rest -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading if ($inner.Found) { foreach ($item in $inner.Settings) { $settings.Add($item) } } } return [pscustomobject]@{ Found = $true; Settings = $settings } } $includes = $null if ($list.Contains('#include')) { $includes = $list['#include'] } if ($includes -isnot [System.Collections.IDictionary]) { return $notFound } foreach ($inc in @($includes.GetEnumerator())) { $usePrefix = $inc.Value -is [System.Collections.IDictionary] -and $inc.Value.prefix -eq $true if (-not $usePrefix -or $inc.Key -ne $segment) { continue } $included = Import-IncludedConfigMap -DirectoryName "$($inc.Key)" -BaseDir $baseDir -Cache $Cache -Loading $Loading if (-not $included) { continue } if ($isLast) { $settings = [System.Collections.Generic.List[object]]::new() if ($IncludeTarget -and $included._settings) { $settings.Add($included._settings) } return [pscustomobject]@{ Found = $true; Settings = $settings } } $inner = Find-ConfigMapAncestorSettings -Node $included -Segments $rest -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading if (-not $inner.Found) { continue } $settings = [System.Collections.Generic.List[object]]::new() if ($included._settings) { $settings.Add($included._settings) } foreach ($item in $inner.Settings) { $settings.Add($item) } return [pscustomobject]@{ Found = $true; Settings = $settings } } foreach ($inc in @($includes.GetEnumerator())) { $usePrefix = $inc.Value -is [System.Collections.IDictionary] -and $inc.Value.prefix -eq $true if ($usePrefix) { continue } $included = Import-IncludedConfigMap -DirectoryName "$($inc.Key)" -BaseDir $baseDir -Cache $Cache -Loading $Loading if (-not $included) { continue } $inner = Find-ConfigMapAncestorSettings -Node $included -Segments $Segments -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading if (-not $inner.Found) { continue } $settings = [System.Collections.Generic.List[object]]::new() if ($included._settings) { $settings.Add($included._settings) } foreach ($item in $inner.Settings) { $settings.Add($item) } return [pscustomobject]@{ Found = $true; Settings = $settings } } return $notFound } function Resolve-ConfigMapEntrySettings { <# .SYNOPSIS Effective settings for an entry: the base, then the map's _settings, then each ancestor's, then the entry's own. Reads no module state. #> param( [Parameter(Mandatory)] $BaseSettings, [System.Collections.IDictionary]$Map, [string]$EntryKey, $Entry, [hashtable]$OperationContext ) $layers = [System.Collections.Generic.List[object]]::new() if ($null -ne $Map -and $Map._settings) { $layers.Add($Map._settings) } if ($null -ne $Map -and -not [string]::IsNullOrEmpty($EntryKey)) { $visited = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) $includeCache = if ($OperationContext) { $OperationContext.IncludeCache } else { @{} } $loadingIncludes = if ($OperationContext) { $OperationContext.LoadingIncludes } else { @{} } $found = Find-ConfigMapAncestorSettings -Node $Map -Segments @($EntryKey -split '\.') -Visited $visited -Cache $includeCache -Loading $loadingIncludes if ($found.Found) { foreach ($ancestorSettings in $found.Settings) { $layers.Add($ancestorSettings) } } } if ($Entry -is [System.Collections.IDictionary] -and $Entry._settings) { $layers.Add($Entry._settings) } $settings = $BaseSettings foreach ($layer in $layers) { $settings = New-ConfigMapSettings -BaseSettings $settings -Overrides $layer } return $settings } function Invoke-WithEntrySettings { <# .SYNOPSIS Run a scriptblock with an entry's effective settings, resolved from -BaseSettings and the map. The caller's current settings are ignored, so a nested qbuild never inherits them. -BaseSettings defaults to the module's base settings; pass the parent entry's settings when -Map is a sub-entry rather than the root map. #> param( [System.Collections.IDictionary]$Map, [string]$EntryKey, $Entry, [hashtable]$OperationContext, $BaseSettings = $script:ConfigMapBaseSettings, [Parameter(Mandatory)] [scriptblock]$ScriptBlock ) $entrySettings = Resolve-ConfigMapEntrySettings ` -BaseSettings $BaseSettings ` -Map $Map ` -EntryKey $EntryKey ` -Entry $Entry ` -OperationContext $OperationContext # Parameter names would hide the caller's $map and $entry from the scriptblock. $settingsScript = $ScriptBlock Remove-Variable Map, EntryKey, Entry, OperationContext, BaseSettings, ScriptBlock -ErrorAction SilentlyContinue # Readers still use Get-ConfigMapSetting, so the resolved settings stay installed for the call. $previousSettings = $script:ConfigMapSettings $script:ConfigMapSettings = $entrySettings try { & $settingsScript } finally { $script:ConfigMapSettings = $previousSettings } } function Get-ConfigMapSettings { return $script:ConfigMapSettings } function Get-ConfigMapSettingsForPath { param( [Parameter(Mandatory)] [System.Collections.IDictionary]$Map, [string]$Path, [hashtable]$OperationContext ) $operationContext = if ($OperationContext) { $OperationContext } else { New-ConfigMapOperationContext } if ([string]::IsNullOrWhiteSpace($Path)) { return Resolve-ConfigMapEntrySettings -BaseSettings $script:ConfigMapBaseSettings -Map $Map -OperationContext $operationContext } # Match qbuild: foo.all inspects the parent entry's effective settings. $entryKey = if ($Path -match '^(.*)\.all$') { $Matches[1] } else { $Path } $targets = @(Get-MapEntries $Map $entryKey -language 'build' -OperationContext $operationContext) if ($targets.Count -eq 0) { throw "Entry '$Path' not found." } return Resolve-ConfigMapEntrySettings ` -BaseSettings $script:ConfigMapBaseSettings ` -Map $Map ` -EntryKey $entryKey ` -Entry $targets[0].Value ` -OperationContext $operationContext } function Get-ConfigMapSetting { param( [Parameter(Mandatory)] [string]$Name ) $settings = Get-ConfigMapSettings return $settings.PSObject.Properties[$Name].Value } function Test-ConfigMapFeatureEnabled { param( [Parameter(Mandatory)] [ValidateSet('TmuxAutoWindow', 'Concurrently', 'DiscoveryCache', 'ForceCheck', 'UseWorkDir')] [string]$Name ) switch (Get-ConfigMapSetting -Name $Name) { $true { return $true } { $_ -in '1', 'true', 'yes', 'on' } { return $true } default { return $false } } } Update-ConfigMapSettings | Out-Null |