diff --git a/CHANGELOG.md b/CHANGELOG.md index c29b636dda..4605669c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - **git:** Use Invoke-Git() with direct path to git.exe to prevent spawning shim subprocesses ([#5122](https://github.com/ScoopInstaller/Scoop/issues/5122), [#5375](https://github.com/ScoopInstaller/Scoop/issues/5375)) - **scoop-download:** Output more detailed manifest information ([#5277](https://github.com/ScoopInstaller/Scoop/issues/5277)) - **core:** Cleanup some old codes, e.g., msi section and config migration ([#5715](https://github.com/ScoopInstaller/Scoop/issues/5715), [#5824](https://github.com/ScoopInstaller/Scoop/issues/5824)) +- **core:** Rewrite and separate path-related functions to `system.ps1` ([#5836](https://github.com/ScoopInstaller/Scoop/issues/5836)) ### Builds diff --git a/bin/uninstall.ps1 b/bin/uninstall.ps1 index 3baf30ba42..9e89481959 100644 --- a/bin/uninstall.ps1 +++ b/bin/uninstall.ps1 @@ -12,6 +12,7 @@ param( ) . "$PSScriptRoot\..\lib\core.ps1" +. "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" . "$PSScriptRoot\..\lib\versions.ps1" @@ -98,7 +99,6 @@ if ($purge) { if ($global) { keep_onlypersist $globaldir } } -remove_from_path (shimdir $false) -if ($global) { remove_from_path (shimdir $true) } +Remove-Path -Path (shimdir $global) -Global:$global success 'Scoop has been uninstalled.' diff --git a/lib/core.ps1 b/lib/core.ps1 index 24b6c8b0c7..d0a312e9b9 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -581,12 +581,18 @@ function fullpath($path) { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path) } function friendly_path($path) { - $h = (Get-PsProvider 'FileSystem').home; if(!$h.endswith('\')) { $h += '\' } - if($h -eq '\') { return $path } - return "$path" -replace ([regex]::escape($h)), "~\" + $h = (Get-PSProvider 'FileSystem').Home + if (!$h.EndsWith('\')) { + $h += '\' + } + if ($h -eq '\') { + return $path + } else { + return $path -replace ([Regex]::Escape($h)), '~\' + } } function is_local($path) { - ($path -notmatch '^https?://') -and (test-path $path) + ($path -notmatch '^https?://') -and (Test-Path $path) } # operations @@ -714,57 +720,6 @@ function Invoke-ExternalCommand { return $true } -function Publish-Env { - if (-not ("Win32.NativeMethods" -as [Type])) { - Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @" -[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] -public static extern IntPtr SendMessageTimeout( - IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, - uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); -"@ - } - - $HWND_BROADCAST = [IntPtr] 0xffff; - $WM_SETTINGCHANGE = 0x1a; - $result = [UIntPtr]::Zero - - [Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, - $WM_SETTINGCHANGE, - [UIntPtr]::Zero, - "Environment", - 2, - 5000, - [ref] $result - ) | Out-Null -} - -function env($name, $global, $val = '__get') { - $RegisterKey = if ($global) { - Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' - } else { - Get-Item -Path 'HKCU:' - } - $EnvRegisterKey = $RegisterKey.OpenSubKey('Environment', $val -ne '__get') - - if ($val -eq '__get') { - $RegistryValueOption = [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames - $EnvRegisterKey.GetValue($name, $null, $RegistryValueOption) - } elseif ($val -eq $null) { - try { $EnvRegisterKey.DeleteValue($name) } catch { } - Publish-Env - } else { - $RegistryValueKind = if ($val.Contains('%')) { - [Microsoft.Win32.RegistryValueKind]::ExpandString - } elseif ($EnvRegisterKey.GetValue($name)) { - $EnvRegisterKey.GetValueKind($name) - } else { - [Microsoft.Win32.RegistryValueKind]::String - } - $EnvRegisterKey.SetValue($name, $val, $RegistryValueKind) - Publish-Env - } -} - function isFileLocked([string]$path) { $file = New-Object System.IO.FileInfo $path @@ -872,7 +827,7 @@ function warn_on_overwrite($shim, $path) { function shim($path, $global, $name, $arg) { if (!(Test-Path $path)) { abort "Can't shim '$(fname $path)': couldn't find '$path'." } $abs_shimdir = ensure (shimdir $global) - ensure_in_path $abs_shimdir $global + Add-Path -Path $abs_shimdir -Global:$global if (!$name) { $name = strip_ext (fname $path) } $shim = "$abs_shimdir\$($name.tolower())" @@ -1009,26 +964,6 @@ function get_shim_path() { return $shim_path } -function search_in_path($target) { - $path = (env 'PATH' $false) + ";" + (env 'PATH' $true) - foreach($dir in $path.split(';')) { - if(test-path "$dir\$target" -pathType leaf) { - return "$dir\$target" - } - } -} - -function ensure_in_path($dir, $global) { - $path = env 'PATH' $global - $dir = fullpath $dir - if($path -notmatch [regex]::escape($dir)) { - write-output "Adding $(friendly_path $dir) to $(if($global){'global'}else{'your'}) path." - - env 'PATH' $global "$dir;$path" # for future sessions... - $env:PATH = "$dir;$env:PATH" # for this session - } -} - function Get-DefaultArchitecture { $arch = get_config DEFAULT_ARCHITECTURE $system = if (${env:ProgramFiles(Arm)}) { @@ -1103,45 +1038,6 @@ function Confirm-InstallationStatus { return , $Installed } -function strip_path($orig_path, $dir) { - if($null -eq $orig_path) { $orig_path = '' } - $stripped = [string]::join(';', @( $orig_path.split(';') | Where-Object { $_ -and $_ -ne $dir } )) - return ($stripped -ne $orig_path), $stripped -} - -function add_first_in_path($dir, $global) { - $dir = fullpath $dir - - # future sessions - $null, $currpath = strip_path (env 'path' $global) $dir - env 'path' $global "$dir;$currpath" - - # this session - $null, $env:PATH = strip_path $env:PATH $dir - $env:PATH = "$dir;$env:PATH" -} - -function remove_from_path($dir, $global) { - $dir = fullpath $dir - - # future sessions - $was_in_path, $newpath = strip_path (env 'path' $global) $dir - if($was_in_path) { - Write-Output "Removing $(friendly_path $dir) from your path." - env 'path' $global $newpath - } - - # current session - $was_in_path, $newpath = strip_path $env:PATH $dir - if($was_in_path) { $env:PATH = $newpath } -} - -function ensure_robocopy_in_path { - if(!(Test-CommandAvailable robocopy)) { - shim "C:\Windows\System32\Robocopy.exe" $false - } -} - function wraptext($text, $width) { if(!$width) { $width = $host.ui.rawui.buffersize.width }; $width -= 1 # be conservative: doesn't seem to print the last char diff --git a/lib/install.ps1 b/lib/install.ps1 index 0ed13707e8..2a244b44c2 100644 --- a/lib/install.ps1 +++ b/lib/install.ps1 @@ -783,7 +783,7 @@ function create_shims($manifest, $dir, $global, $arch) { } elseif (Test-Path $target -PathType leaf) { $bin = $target } else { - $bin = search_in_path $target + $bin = (Get-Command $target).Source } if (!$bin) { abort "Can't shim '$target': File doesn't exist." } @@ -876,16 +876,16 @@ function unlink_current($versiondir) { # to undo after installers add to path so that scoop manifest can keep track of this instead function ensure_install_dir_not_in_path($dir, $global) { - $path = (env 'path' $global) + $path = (Get-EnvVar -Name 'PATH' -Global:$global) $fixed, $removed = find_dir_or_subdir $path "$dir" if ($removed) { $removed | ForEach-Object { "Installer added '$(friendly_path $_)' to path. Removing." } - env 'path' $global $fixed + Set-EnvVar -Name 'PATH' -Value $fixed -Global:$global } if (!$global) { - $fixed, $removed = find_dir_or_subdir (env 'path' $true) "$dir" + $fixed, $removed = find_dir_or_subdir (Get-EnvVar -Name 'PATH' -Global) "$dir" if ($removed) { $removed | ForEach-Object { warn "Installer added '$_' to system path. You might want to remove this manually (requires admin permission)." } } @@ -920,7 +920,7 @@ function env_add_path($manifest, $dir, $global, $arch) { if (!(is_in_dir $dir $path_dir)) { abort "Error in manifest: env_add_path '$_' is outside the app directory." } - add_first_in_path $path_dir $global + Add-Path -Path $path_dir -Global:$global -Force } } } @@ -935,7 +935,7 @@ function env_rm_path($manifest, $dir, $global, $arch) { } else { $path_dir = Join-Path $dir $_ } - remove_from_path $path_dir $global + Remove-Path -Path $path_dir -Global:$global } } } @@ -946,7 +946,7 @@ function env_set($manifest, $dir, $global, $arch) { $env_set | Get-Member -Member NoteProperty | ForEach-Object { $name = $_.name $val = format $env_set.$($_.name) @{ 'dir' = $dir } - env $name $global $val + Set-EnvVar -Name $name -Value $val -Global:$global Set-Content env:\$name $val } } @@ -956,7 +956,7 @@ function env_rm($manifest, $global, $arch) { if ($env_set) { $env_set | Get-Member -Member NoteProperty | ForEach-Object { $name = $_.name - env $name $global $null + Set-EnvVar -Name $name -Value $null -Global:$global if (Test-Path env:\$name) { Remove-Item env:\$name } } } diff --git a/lib/psmodules.ps1 b/lib/psmodules.ps1 index c65fd65c7b..3ba2f6624a 100644 --- a/lib/psmodules.ps1 +++ b/lib/psmodules.ps1 @@ -42,7 +42,7 @@ function uninstall_psmodule($manifest, $dir, $global) { } function ensure_in_psmodulepath($dir, $global) { - $path = env 'psmodulepath' $global + $path = Get-EnvVar -Name 'PSModulePath' -Global:$global if (!$global -and $null -eq $path) { $path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" } @@ -50,7 +50,6 @@ function ensure_in_psmodulepath($dir, $global) { if ($path -notmatch [Regex]::Escape($dir)) { Write-Output "Adding $(friendly_path $dir) to $(if($global){'global'}else{'your'}) PowerShell module path." - env 'psmodulepath' $global "$dir;$path" # for future sessions... - $env:psmodulepath = "$dir;$env:psmodulepath" # for this session + Set-EnvVar -Name 'PSModulePath' -Value "$dir;$path" -Global:$global } } diff --git a/lib/system.ps1 b/lib/system.ps1 new file mode 100644 index 0000000000..aa26cf2af0 --- /dev/null +++ b/lib/system.ps1 @@ -0,0 +1,158 @@ +# System-related functions + +## Environment Variables + +function Publish-EnvVar { + if (-not ('Win32.NativeMethods' -as [Type])) { + Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @' +[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] +public static extern IntPtr SendMessageTimeout( + IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, + uint fuFlags, uint uTimeout, out UIntPtr lpdwResult +); +'@ + } + + $HWND_BROADCAST = [IntPtr] 0xffff + $WM_SETTINGCHANGE = 0x1a + $result = [UIntPtr]::Zero + + [Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, + $WM_SETTINGCHANGE, + [UIntPtr]::Zero, + 'Environment', + 2, + 5000, + [ref] $result + ) | Out-Null +} + +function Get-EnvVar { + param( + [string]$Name, + [switch]$Global + ) + + $registerKey = if ($Global) { + Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' + } else { + Get-Item -Path 'HKCU:' + } + $envRegisterKey = $registerKey.OpenSubKey('Environment') + $registryValueOption = [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames + $envRegisterKey.GetValue($Name, $null, $registryValueOption) +} + +function Set-EnvVar { + param( + [string]$Name, + [string]$Value, + [switch]$Global + ) + + $registerKey = if ($Global) { + Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' + } else { + Get-Item -Path 'HKCU:' + } + $envRegisterKey = $registerKey.OpenSubKey('Environment', $true) + if ($null -eq $Value -or $Value -eq '') { + $envRegisterKey.DeleteValue($Name) + } else { + $registryValueKind = if ($Value.Contains('%')) { + [Microsoft.Win32.RegistryValueKind]::ExpandString + } elseif ($envRegisterKey.GetValue($Name)) { + $envRegisterKey.GetValueKind($Name) + } else { + [Microsoft.Win32.RegistryValueKind]::String + } + $envRegisterKey.SetValue($Name, $Value, $registryValueKind) + } + Publish-EnvVar +} + +function Test-PathLikeEnvVar { + param( + [string]$Name, + [string]$Path + ) + + if ($null -eq $Path -and $Path -eq '') { + return $false, $null + } else { + $strippedPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries).Where({ $_ -ne $Name }) -join ';' + return ($strippedPath -ne $Path), $strippedPath + } +} + +function Add-Path { + param( + [string]$Path, + [switch]$Global, + [switch]$Force + ) + + if (!$Path.Contains('%')) { + $Path = fullpath $Path + } + # future sessions + $inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global) + if (!$inPath -or $Force) { + Write-Output "Adding $(friendly_path $Path) to $(if ($Global) {'global'} else {'your'}) path." + Set-EnvVar -Name 'PATH' -Value (@($Path, $strippedPath) -join ';') -Global:$Global + } + # current session + $inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH + if (!$inPath -or $Force) { + $env:PATH = @($Path, $strippedPath) -join ';' + } +} + +function Remove-Path { + param( + [string]$Path, + [switch]$Global + ) + + if (!$Path.Contains('%')) { + $Path = fullpath $Path + } + # future sessions + $inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global) + if ($inPath) { + Write-Output "Removing $(friendly_path $Path) from $(if ($Global) {'global'} else {'your'}) path." + Set-EnvVar -Name 'PATH' -Value $strippedPath -Global:$Global + } + # current session + $inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH + if ($inPath) { + $env:PATH = $strippedPath + } +} + +## Deprecated functions + +function env($name, $global, $val) { + if ($PSBoundParameters.ContainsKey('val')) { + Show-DeprecatedWarning $MyInvocation 'Set-EnvVar' + Set-EnvVar -Name $name -Value $val -Global:$global + } else { + Show-DeprecatedWarning $MyInvocation 'Get-EnvVar' + Get-EnvVar -Name $name -Global:$global + } +} + +function strip_path($orig_path, $dir) { + Show-DeprecatedWarning $MyInvocation 'Test-PathLikeEnvVar' + Test-PathLikeEnvVar -Name $dir -Path $orig_path +} + +function add_first_in_path($dir, $global) { + Show-DeprecatedWarning $MyInvocation 'Add-Path' + Add-Path -Path $dir -Global:$global -Force +} + +function remove_from_path($dir, $global) { + Show-DeprecatedWarning $MyInvocation 'Remove-Path' + Remove-Path -Path $dir -Global:$global +} diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1 index 61cad28883..fac03d71f4 100644 --- a/libexec/scoop-install.ps1 +++ b/libexec/scoop-install.ps1 @@ -25,6 +25,7 @@ . "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' 'manifest.ps1' (indirectly) . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) . "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly) +. "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\decompress.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" diff --git a/libexec/scoop-reset.ps1 b/libexec/scoop-reset.ps1 index c30eeb6e1b..e073cd4dfb 100644 --- a/libexec/scoop-reset.ps1 +++ b/libexec/scoop-reset.ps1 @@ -8,6 +8,7 @@ . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" # 'Select-CurrentVersion' (indirectly) +. "$PSScriptRoot\..\lib\system.ps1" # 'env_add_path' (indirectly) . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' . "$PSScriptRoot\..\lib\shortcuts.ps1" diff --git a/libexec/scoop-uninstall.ps1 b/libexec/scoop-uninstall.ps1 index 44f5561db5..7931158eec 100644 --- a/libexec/scoop-uninstall.ps1 +++ b/libexec/scoop-uninstall.ps1 @@ -8,6 +8,7 @@ . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 'Select-CurrentVersion' (indirectly) +. "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" . "$PSScriptRoot\..\lib\psmodules.ps1" diff --git a/libexec/scoop-update.ps1 b/libexec/scoop-update.ps1 index e632be72fb..517aeeed1e 100644 --- a/libexec/scoop-update.ps1 +++ b/libexec/scoop-update.ps1 @@ -16,6 +16,7 @@ . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\json.ps1" # 'save_install_info' in 'manifest.ps1' (indirectly) +. "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" . "$PSScriptRoot\..\lib\psmodules.ps1" . "$PSScriptRoot\..\lib\decompress.ps1" diff --git a/test/Scoop-Core.Tests.ps1 b/test/Scoop-Core.Tests.ps1 index 4c95067e23..3fb7fbd663 100644 --- a/test/Scoop-Core.Tests.ps1 +++ b/test/Scoop-Core.Tests.ps1 @@ -1,6 +1,7 @@ BeforeAll { . "$PSScriptRoot\Scoop-TestLib.ps1" . "$PSScriptRoot\..\lib\core.ps1" + . "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\install.ps1" } @@ -167,7 +168,7 @@ Describe 'shim' -Tag 'Scoop', 'Windows' { BeforeAll { $working_dir = setup_working 'shim' $shimdir = shimdir - $(ensure_in_path $shimdir) | Out-Null + Add-Path $shimdir } It "links a file onto the user's path" { @@ -201,7 +202,7 @@ Describe 'rm_shim' -Tag 'Scoop', 'Windows' { BeforeAll { $working_dir = setup_working 'shim' $shimdir = shimdir - $(ensure_in_path $shimdir) | Out-Null + Add-Path $shimdir } It 'removes shim from path' { @@ -220,7 +221,7 @@ Describe 'get_app_name_from_shim' -Tag 'Scoop', 'Windows' { BeforeAll { $working_dir = setup_working 'shim' $shimdir = shimdir - $(ensure_in_path $shimdir) | Out-Null + Add-Path $shimdir Mock appsdir { $working_dir } } @@ -258,36 +259,6 @@ Describe 'get_app_name_from_shim' -Tag 'Scoop', 'Windows' { } } -Describe 'ensure_robocopy_in_path' -Tag 'Scoop', 'Windows' { - BeforeAll { - $shimdir = shimdir $false - Mock versiondir { "$PSScriptRoot\.." } - } - - It 'shims robocopy when not on path' { - Mock Test-CommandAvailable { $false } - Test-CommandAvailable robocopy | Should -Be $false - - ensure_robocopy_in_path - - # "$shimdir/robocopy.ps1" | should -exist - "$shimdir/robocopy.exe" | Should -Exist - - # clean up - rm_shim robocopy $(shimdir $false) | Out-Null - } - - It 'does not shim robocopy when it is in path' { - Mock Test-CommandAvailable { $true } - Test-CommandAvailable robocopy | Should -Be $true - - ensure_robocopy_in_path - - # "$shimdir/robocopy.ps1" | should -not -exist - "$shimdir/robocopy.exe" | Should -Not -Exist - } -} - Describe 'sanitary_path' -Tag 'Scoop' { It 'removes invalid path characters from a string' { $path = 'test?.json' diff --git a/test/Scoop-Install.Tests.ps1 b/test/Scoop-Install.Tests.ps1 index bafdfbe847..140327d11f 100644 --- a/test/Scoop-Install.Tests.ps1 +++ b/test/Scoop-Install.Tests.ps1 @@ -1,6 +1,7 @@ BeforeAll { . "$PSScriptRoot\Scoop-TestLib.ps1" . "$PSScriptRoot\..\lib\core.ps1" + . "$PSScriptRoot\..\lib\system.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" . "$PSScriptRoot\..\lib\install.ps1" } @@ -57,17 +58,17 @@ Describe 'env add and remove path' -Tag 'Scoop', 'Windows' { } It 'should concat the correct path' { - Mock add_first_in_path {} - Mock remove_from_path {} + Mock Add-Path {} + Mock Remove-Path {} # adding env_add_path $manifest $testdir $global - Assert-MockCalled add_first_in_path -Times 1 -ParameterFilter { $dir -like "$testdir\foo" } - Assert-MockCalled add_first_in_path -Times 1 -ParameterFilter { $dir -like "$testdir\bar" } + Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" } + Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" } env_rm_path $manifest $testdir $global - Assert-MockCalled remove_from_path -Times 1 -ParameterFilter { $dir -like "$testdir\foo" } - Assert-MockCalled remove_from_path -Times 1 -ParameterFilter { $dir -like "$testdir\bar" } + Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" } + Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" } } }