Support/ExecutionCore/Eigenverft.Manifested.Package.ExecutionCore.PathRegistration.ps1
|
<#
Eigenverft.Manifested.Package.ExecutionCore.PathRegistration #> function Get-EnvironmentVariableValue { <# .SYNOPSIS Reads an environment variable for one target scope. .DESCRIPTION Returns the current environment variable value for the requested Process, User, or Machine target. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [ValidateSet('Process', 'User', 'Machine')] [string]$Target ) return [Environment]::GetEnvironmentVariable($Name, $Target) } function Sync-ProcessPathEnvironment { <# .SYNOPSIS Rebuilds the current process PATH from persisted Machine and User PATH. .DESCRIPTION Central PATH sync used whenever persisted User/Machine PATH changes and before command resolution in long-lived shells. Composes Process PATH as Machine entries then User entries (deduplicated), then keeps process-only extras that are not listed in -RemoveDirectories. This makes Assign PATH changes visible to later Package work in the same or other sessions after sync, without requiring a new console. .PARAMETER RemoveDirectories Optional directories that must not remain on Process PATH even if they were process-only extras (used during PATH unregistration). #> [CmdletBinding()] param( [AllowEmptyCollection()] [string[]]$RemoveDirectories = @() ) $normalizedRemove = @( foreach ($directoryPath in @($RemoveDirectories)) { $normalizedDirectoryPath = Get-NormalizedPathEntry -PathEntry $directoryPath if (-not [string]::IsNullOrWhiteSpace($normalizedDirectoryPath)) { $normalizedDirectoryPath } } ) $machineValue = Get-EnvironmentVariableValue -Name 'Path' -Target 'Machine' $userValue = Get-EnvironmentVariableValue -Name 'Path' -Target 'User' $processValue = Get-EnvironmentVariableValue -Name 'Path' -Target 'Process' $composedEntries = New-Object System.Collections.Generic.List[string] $seenNormalized = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) foreach ($scopeValue in @($machineValue, $userValue)) { foreach ($entry in @(([string]$scopeValue) -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } $trimmedEntry = $entry.Trim() $normalizedEntry = Get-NormalizedPathEntry -PathEntry $trimmedEntry if ([string]::IsNullOrWhiteSpace($normalizedEntry)) { continue } if ($normalizedEntry -in $normalizedRemove) { continue } if ($seenNormalized.Add($normalizedEntry)) { $composedEntries.Add($trimmedEntry) | Out-Null } } } foreach ($entry in @(([string]$processValue) -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } $trimmedEntry = $entry.Trim() $normalizedEntry = Get-NormalizedPathEntry -PathEntry $trimmedEntry if ([string]::IsNullOrWhiteSpace($normalizedEntry)) { continue } if ($normalizedEntry -in $normalizedRemove) { continue } if ($seenNormalized.Contains($normalizedEntry)) { continue } # Keep process-only extras (for example temporary tool dirs) unless explicitly removed. [void]$seenNormalized.Add($normalizedEntry) $composedEntries.Add($trimmedEntry) | Out-Null } $composedValue = (@($composedEntries.ToArray()) -join ';') $currentNormalizedProcessEntries = @( foreach ($entry in @(([string]$processValue) -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } Get-NormalizedPathEntry -PathEntry $entry.Trim() } ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } $composedNormalizedEntries = @( foreach ($entry in @($composedEntries.ToArray())) { Get-NormalizedPathEntry -PathEntry $entry } ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } $changed = ($currentNormalizedProcessEntries.Count -ne $composedNormalizedEntries.Count) if (-not $changed) { for ($i = 0; $i -lt $composedNormalizedEntries.Count; $i++) { if (-not [string]::Equals( [string]$currentNormalizedProcessEntries[$i], [string]$composedNormalizedEntries[$i], [System.StringComparison]::OrdinalIgnoreCase)) { $changed = $true break } } } if ($changed) { # Write Process scope only through the helper so $env:Path stays aligned; do not # recurse into persisted User/Machine sync from Sync itself. [Environment]::SetEnvironmentVariable('Path', $composedValue, 'Process') $env:Path = $composedValue } return [pscustomobject]@{ Changed = $changed Value = $composedValue Target = 'Process' } } function Set-EnvironmentVariableValue { <# .SYNOPSIS Writes an environment variable for one target scope. .DESCRIPTION Persists the requested environment variable value to the Process, User, or Machine target scope. When Path is written to User or Machine, Process PATH is always resynchronized afterward via Sync-ProcessPathEnvironment so the current session immediately reflects persisted PATH changes. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [AllowNull()] [string]$Value, [Parameter(Mandatory = $true)] [ValidateSet('Process', 'User', 'Machine')] [string]$Target, [AllowEmptyCollection()] [string[]]$SyncRemoveDirectories = @() ) [Environment]::SetEnvironmentVariable($Name, $Value, $Target) if ([string]::Equals($Name, 'Path', [System.StringComparison]::OrdinalIgnoreCase) -and $Target -eq 'Process') { $env:Path = $Value } if ([string]::Equals($Name, 'Path', [System.StringComparison]::OrdinalIgnoreCase) -and $Target -in @('User', 'Machine')) { $null = Sync-ProcessPathEnvironment -RemoveDirectories $SyncRemoveDirectories } } function Get-NormalizedPathEntry { [CmdletBinding()] param( [AllowNull()] [string]$PathEntry ) if ([string]::IsNullOrWhiteSpace($PathEntry)) { return $null } $expandedEntry = [Environment]::ExpandEnvironmentVariables($PathEntry.Trim()) -replace '/', '\' if ([string]::IsNullOrWhiteSpace($expandedEntry)) { return $null } if ([System.IO.Path]::IsPathRooted($expandedEntry)) { try { return [System.IO.Path]::GetFullPath($expandedEntry).TrimEnd('\') } catch { return $expandedEntry.TrimEnd('\') } } return $expandedEntry.TrimEnd('\') } function Resolve-PathRegistrationDirectory { <# .SYNOPSIS Resolves the directory that should be added to PATH. .DESCRIPTION Turns a raw source path into the concrete directory entry that should be registered in PATH. Existing files resolve to their parent directory. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourcePath, [string]$SourceKind ) $resolvedSourcePath = [System.IO.Path]::GetFullPath($SourcePath) if ($SourceKind -in @('commandEntryPoint', 'appEntryPoint', 'shim')) { return [System.IO.Path]::GetFullPath((Split-Path -Parent $resolvedSourcePath)) } if (Test-Path -LiteralPath $resolvedSourcePath -PathType Leaf) { return [System.IO.Path]::GetFullPath((Split-Path -Parent $resolvedSourcePath)) } return [System.IO.Path]::GetFullPath($resolvedSourcePath) } function Add-PathEntry { [CmdletBinding()] param( [AllowNull()] [string]$CurrentValue, [Parameter(Mandatory = $true)] [string]$DirectoryPath ) $normalizedTargetDirectory = Get-NormalizedPathEntry -PathEntry $DirectoryPath $existingEntries = @() foreach ($entry in @(([string]$CurrentValue) -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } $existingEntries += $entry.Trim() } foreach ($entry in @($existingEntries)) { $normalizedEntry = Get-NormalizedPathEntry -PathEntry $entry if ([string]::Equals($normalizedEntry, $normalizedTargetDirectory, [System.StringComparison]::OrdinalIgnoreCase)) { return [pscustomobject]@{ Value = ($existingEntries -join ';') Changed = $false } } } $updatedEntries = @($existingEntries) + @($DirectoryPath) return [pscustomobject]@{ Value = ($updatedEntries -join ';') Changed = $true } } function Remove-PathEntries { [CmdletBinding()] param( [AllowNull()] [string]$CurrentValue, [string[]]$DirectoryPaths ) $normalizedDirectoriesToRemove = @( foreach ($directoryPath in @($DirectoryPaths)) { $normalizedDirectoryPath = Get-NormalizedPathEntry -PathEntry $directoryPath if (-not [string]::IsNullOrWhiteSpace($normalizedDirectoryPath)) { $normalizedDirectoryPath } } ) if (@($normalizedDirectoriesToRemove).Count -eq 0) { return [pscustomobject]@{ Value = [string]$CurrentValue Changed = $false RemovedEntries = @() } } $filteredEntries = New-Object System.Collections.Generic.List[string] $removedEntries = New-Object System.Collections.Generic.List[string] foreach ($entry in @(([string]$CurrentValue) -split ';')) { if ([string]::IsNullOrWhiteSpace($entry)) { continue } $trimmedEntry = $entry.Trim() $normalizedEntry = Get-NormalizedPathEntry -PathEntry $trimmedEntry if ($normalizedEntry -and $normalizedEntry -in $normalizedDirectoriesToRemove) { $removedEntries.Add($trimmedEntry) | Out-Null continue } $filteredEntries.Add($trimmedEntry) | Out-Null } return [pscustomobject]@{ Value = (@($filteredEntries.ToArray()) -join ';') Changed = ($removedEntries.Count -gt 0) RemovedEntries = @($removedEntries.ToArray()) } } function Remove-PathEnvironmentDirectories { <# .SYNOPSIS Removes directory entries from PATH for the requested scopes without adding a new entry. .DESCRIPTION Used by Package removal to drop previously registered directories from Process and User or Machine PATH. Persisted User/Machine Path writes also resync Process via Sync-ProcessPathEnvironment. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('user', 'machine')] [string]$Mode, [AllowEmptyCollection()] [string[]]$DirectoryPaths ) $directoriesToRemove = @( foreach ($directoryPath in @($DirectoryPaths)) { $normalizedDirectoryPath = Get-NormalizedPathEntry -PathEntry $directoryPath if (-not [string]::IsNullOrWhiteSpace($normalizedDirectoryPath)) { $normalizedDirectoryPath } } ) $targets = @('Process') if ($Mode -eq 'user') { $targets += 'User' } elseif ($Mode -eq 'machine') { $targets += 'Machine' } $updatedTargets = New-Object System.Collections.Generic.List[string] $cleanedTargets = New-Object System.Collections.Generic.List[string] foreach ($target in @($targets)) { $currentValue = Get-EnvironmentVariableValue -Name 'Path' -Target $target $cleanupResult = Remove-PathEntries -CurrentValue $currentValue -DirectoryPaths $directoriesToRemove if ($cleanupResult.Changed) { if ($target -in @('User', 'Machine')) { Set-EnvironmentVariableValue -Name 'Path' -Value $cleanupResult.Value -Target $target -SyncRemoveDirectories $directoriesToRemove } else { Set-EnvironmentVariableValue -Name 'Path' -Value $cleanupResult.Value -Target $target } $updatedTargets.Add($target) | Out-Null $cleanedTargets.Add($target) | Out-Null } } return [pscustomobject]@{ Status = if ($updatedTargets.Count -gt 0) { 'Unregistered' } else { 'AlreadyAbsent' } Mode = $Mode CleanedTargets = @($cleanedTargets.ToArray()) UpdatedTargets = @($updatedTargets.ToArray()) RemovedDirectories = @($directoriesToRemove) } } function Register-PathEnvironment { <# .SYNOPSIS Registers a directory in PATH for the requested scopes. .DESCRIPTION Updates Process and User PATH for user mode, or Process and Machine PATH for machine mode. Persisted User/Machine Path writes always resynchronize Process PATH afterward via Sync-ProcessPathEnvironment. Removes any requested cleanup directories before ensuring the active directory is present. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('user', 'machine')] [string]$Mode, [Parameter(Mandatory = $true)] [string]$RegisteredPath, [string[]]$CleanupDirectories ) if ([string]::IsNullOrWhiteSpace($RegisteredPath)) { throw 'PATH registration requires a registered directory path.' } if (-not (Test-Path -LiteralPath $RegisteredPath)) { throw "PATH registration directory '$RegisteredPath' was not found." } $normalizedRegisteredPath = Resolve-PathRegistrationDirectory -SourcePath $RegisteredPath $cleanupDirectoriesToApply = @($CleanupDirectories) $targets = @('Process') if ($Mode -eq 'user') { $targets += 'User' } elseif ($Mode -eq 'machine') { $targets += 'Machine' } $updatedTargets = New-Object System.Collections.Generic.List[string] $cleanedTargets = New-Object System.Collections.Generic.List[string] foreach ($target in @($targets)) { $currentValue = Get-EnvironmentVariableValue -Name 'Path' -Target $target $cleanupResult = Remove-PathEntries -CurrentValue $currentValue -DirectoryPaths $cleanupDirectoriesToApply $updateResult = Add-PathEntry -CurrentValue $cleanupResult.Value -DirectoryPath $normalizedRegisteredPath if ($cleanupResult.Changed -or $updateResult.Changed) { if ($target -in @('User', 'Machine')) { Set-EnvironmentVariableValue -Name 'Path' -Value $updateResult.Value -Target $target -SyncRemoveDirectories $cleanupDirectoriesToApply } else { Set-EnvironmentVariableValue -Name 'Path' -Value $updateResult.Value -Target $target } $updatedTargets.Add($target) | Out-Null } if ($cleanupResult.Changed) { $cleanedTargets.Add($target) | Out-Null } } return [pscustomobject]@{ Status = if ($updatedTargets.Count -gt 0) { 'Registered' } else { 'AlreadyRegistered' } Mode = $Mode RegisteredPath = $normalizedRegisteredPath CleanupDirectories = @($cleanupDirectoriesToApply) CleanedTargets = @($cleanedTargets.ToArray()) UpdatedTargets = @($updatedTargets.ToArray()) } } |