Skip to content

Commit

Permalink
feat(path): Isolate Scoop apps' PATH (#5840)
Browse files Browse the repository at this point in the history
  • Loading branch information
niheaven committed Apr 18, 2024
1 parent fa06e92 commit 5819b5a
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 56 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@
- **bucket:** Make official buckets higher priority ([#5398](https://github.com/ScoopInstaller/Scoop/issues/5398))
- **core:** Add `-Quiet` switch for `Invoke-ExternalCommand` ([#5346](https://github.com/ScoopInstaller/Scoop/issues/5346))
- **core:** Allow global install of PowerShell modules ([#5611](https://github.com/ScoopInstaller/Scoop/issues/5611))
- **path:** Isolate Scoop apps' PATH ([#5840](https://github.com/ScoopInstaller/Scoop/issues/5840))

### Bug Fixes

Expand Down
3 changes: 3 additions & 0 deletions bin/uninstall.ps1
Expand Up @@ -100,5 +100,8 @@ if ($purge) {
}

Remove-Path -Path (shimdir $global) -Global:$global
if (get_config USE_ISOLATED_PATH) {
Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
}

success 'Scoop has been uninstalled.'
80 changes: 79 additions & 1 deletion lib/core.ps1
Expand Up @@ -132,6 +132,9 @@ function set_config {
$value = [System.Convert]::ToBoolean($value)
}

# Initialize config's change
Complete-ConfigChange -Name $name -Value $value

if ($null -eq $scoopConfig.$name) {
$scoopConfig | Add-Member -MemberType NoteProperty -Name $name -Value $value
} else {
Expand All @@ -147,6 +150,74 @@ function set_config {
return $scoopConfig
}

function Complete-ConfigChange {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0)]
[string]
$Name,
[Parameter(Mandatory, Position = 1)]
[AllowEmptyString()]
[string]
$Value
)

if ($Name -eq 'use_isolated_path') {
$oldValue = get_config USE_ISOLATED_PATH
if ($Value -eq $oldValue) {
return
} else {
$currPathEnvVar = $scoopPathEnvVar
}
. "$PSScriptRoot\..\lib\system.ps1"

if ($Value -eq $false -or $Value -eq '') {
info 'Turn off Scoop isolated path... This may take a while, please wait.'
$movedPath = Get-EnvVar -Name $currPathEnvVar
if ($movedPath) {
Add-Path -Path $movedPath -Quiet
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Quiet
Set-EnvVar -Name $currPathEnvVar -Quiet
}
if (is_admin) {
$movedPath = Get-EnvVar -Name $currPathEnvVar -Global
if ($movedPath) {
Add-Path -Path $movedPath -Global -Quiet
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Global -Quiet
Set-EnvVar -Name $currPathEnvVar -Global -Quiet
}
}
} else {
$newPathEnvVar = if ($Value -eq $true) {
'SCOOP_PATH'
} else {
$Value.ToUpperInvariant()
}
info "Turn on Scoop isolated path ('$newPathEnvVar')... This may take a while, please wait."
$movedPath = Remove-Path -Path "$scoopdir\apps\*" -TargetEnvVar $currPathEnvVar -Quiet -PassThru
if ($movedPath) {
Add-Path -Path $movedPath -TargetEnvVar $newPathEnvVar -Quiet
Add-Path -Path ('%' + $newPathEnvVar + '%') -Quiet
if ($currPathEnvVar -ne 'PATH') {
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Quiet
Set-EnvVar -Name $currPathEnvVar -Quiet
}
}
if (is_admin) {
$movedPath = Remove-Path -Path "$globaldir\apps\*" -TargetEnvVar $currPathEnvVar -Global -Quiet -PassThru
if ($movedPath) {
Add-Path -Path $movedPath -TargetEnvVar $newPathEnvVar -Global -Quiet
Add-Path -Path ('%' + $newPathEnvVar + '%') -Global -Quiet
if ($currPathEnvVar -ne 'PATH') {
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Global -Quiet
Set-EnvVar -Name $currPathEnvVar -Global -Quiet
}
}
}
}
}
}

function setup_proxy() {
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
$proxy = get_config PROXY
Expand Down Expand Up @@ -303,7 +374,7 @@ function filesize($length) {
} else {
if ($null -eq $length) {
$length = 0
}
}
"$($length) B"
}
}
Expand Down Expand Up @@ -1350,6 +1421,13 @@ $globaldir = $env:SCOOP_GLOBAL, (get_config GLOBAL_PATH), "$([System.Environment
# Use at your own risk.
$cachedir = $env:SCOOP_CACHE, (get_config CACHE_PATH), "$scoopdir\cache" | Where-Object { $_ } | Select-Object -First 1 | Get-AbsolutePath

# Scoop apps' PATH Environment Variable
$scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
{ $_ -is [string] } { $_.ToUpperInvariant() }
$true { 'SCOOP_PATH' }
default { 'PATH' }
}

# OS information
$WindowsBuild = [System.Environment]::OSVersion.Version.Build

Expand Down
27 changes: 7 additions & 20 deletions lib/install.ps1
Expand Up @@ -906,34 +906,21 @@ function env_add_path($manifest, $dir, $global, $arch) {
$env_add_path = arch_specific 'env_add_path' $manifest $arch
$dir = $dir.TrimEnd('\')
if ($env_add_path) {
# GH-3785: Add path in ascending order.
[Array]::Reverse($env_add_path)
$env_add_path | Where-Object { $_ } | ForEach-Object {
if ($_ -eq '.') {
$path_dir = $dir
} else {
$path_dir = Join-Path $dir $_
}
if (!(is_in_dir $dir $path_dir)) {
abort "Error in manifest: env_add_path '$_' is outside the app directory."
}
Add-Path -Path $path_dir -Global:$global -Force
if (get_config USE_ISOLATED_PATH) {
Add-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
}
$path = $env_add_path.Where({ $_ }).ForEach({ Join-Path $dir $_ | Get-AbsolutePath }).Where({ is_in_dir $dir $_ })
Add-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global -Force
}
}

function env_rm_path($manifest, $dir, $global, $arch) {
$env_add_path = arch_specific 'env_add_path' $manifest $arch
$dir = $dir.TrimEnd('\')
if ($env_add_path) {
$env_add_path | Where-Object { $_ } | ForEach-Object {
if ($_ -eq '.') {
$path_dir = $dir
} else {
$path_dir = Join-Path $dir $_
}
Remove-Path -Path $path_dir -Global:$global
}
$path = $env_add_path.Where({ $_ }).ForEach({ Join-Path $dir $_ | Get-AbsolutePath }).Where({ is_in_dir $dir $_ })
Remove-Path -Path $path -Global:$global # TODO: Remove after forced isolating Scoop path
Remove-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global
}
}

Expand Down
70 changes: 43 additions & 27 deletions lib/system.ps1
Expand Up @@ -73,63 +73,79 @@ function Set-EnvVar {
Publish-EnvVar
}

function Test-PathLikeEnvVar {
function Split-PathLikeEnvVar {
param(
[string]$Name,
[string[]]$Pattern,
[string]$Path
)

if ($null -eq $Path -and $Path -eq '') {
return $false, $null
return $null, $null
} else {
$strippedPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries).Where({ $_ -ne $Name }) -join ';'
return ($strippedPath -ne $Path), $strippedPath
$splitPattern = $Pattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)
$splitPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)
$inPath = @()
foreach ($p in $splitPattern) {
$inPath += $splitPath.Where({ $_ -like $p })
$splitPath = $splitPath.Where({ $_ -notlike $p })
}
return ($inPath -join ';'), ($splitPath -join ';')
}
}

function Add-Path {
param(
[string]$Path,
[string[]]$Path,
[string]$TargetEnvVar = 'PATH',
[switch]$Global,
[switch]$Force
[switch]$Force,
[switch]$Quiet
)

if (!$Path.Contains('%')) {
$Path = Get-AbsolutePath $Path
}
# future sessions
$inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global)
$inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -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
if (!$Quiet) {
$Path | ForEach-Object {
Write-Host "Adding $(friendly_path $_) to $(if ($Global) {'global'} else {'your'}) path."
}
}
Set-EnvVar -Name $TargetEnvVar -Value ((@($Path) + $strippedPath) -join ';') -Global:$Global
}
# current session
$inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH
$inPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH
if (!$inPath -or $Force) {
$env:PATH = @($Path, $strippedPath) -join ';'
$env:PATH = (@($Path) + $strippedPath) -join ';'
}
}

function Remove-Path {
param(
[string]$Path,
[switch]$Global
[string[]]$Path,
[string]$TargetEnvVar = 'PATH',
[switch]$Global,
[switch]$Quiet,
[switch]$PassThru
)

if (!$Path.Contains('%')) {
$Path = Get-AbsolutePath $Path
}
# future sessions
$inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global)
$inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -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
if (!$Quiet) {
$Path | ForEach-Object {
Write-Host "Removing $(friendly_path $_) from $(if ($Global) {'global'} else {'your'}) path."
}
}
Set-EnvVar -Name $TargetEnvVar -Value $strippedPath -Global:$Global
}
# current session
$inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH
if ($inPath) {
$inSessionPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH
if ($inSessionPath) {
$env:PATH = $strippedPath
}
if ($PassThru) {
return $inPath
}
}

## Deprecated functions
Expand All @@ -145,8 +161,8 @@ function env($name, $global, $val) {
}

function strip_path($orig_path, $dir) {
Show-DeprecatedWarning $MyInvocation 'Test-PathLikeEnvVar'
Test-PathLikeEnvVar -Name $dir -Path $orig_path
Show-DeprecatedWarning $MyInvocation 'Split-PathLikeEnvVar'
Split-PathLikeEnvVar -Name $dir -Path $orig_path
}

function add_first_in_path($dir, $global) {
Expand Down
5 changes: 5 additions & 0 deletions libexec/scoop-config.ps1
Expand Up @@ -115,6 +115,11 @@
# Nightly version is formatted as 'nightly-yyyyMMdd' and will be updated after one day if this is set to $true.
# Otherwise, nightly version will not be updated unless `--force` is used.
#
# use_isolated_path: $true|$false|[string]
# When set to $true, Scoop will use `SCOOP_PATH` environment variable to store apps' `PATH`s.
# When set to arbitrary non-empty string, Scoop will use that string as the environment variable name instead.
# This is useful when you want to isolate Scoop from the system `PATH`.
#
# ARIA2 configuration
# -------------------
#
Expand Down
3 changes: 3 additions & 0 deletions libexec/scoop-reset.ps1
Expand Up @@ -80,6 +80,9 @@ $apps | ForEach-Object {
$dir = link_current $dir
create_shims $manifest $dir $global $architecture
create_startmenu_shortcuts $manifest $dir $global $architecture
# unset all potential old env before re-adding
env_rm_path $manifest $dir $global $architecture
env_rm $manifest $global $architecture
env_add_path $manifest $dir $global $architecture
env_set $manifest $dir $global $architecture
# unlink all potential old link before re-persisting
Expand Down
17 changes: 9 additions & 8 deletions test/Scoop-Install.Tests.ps1
Expand Up @@ -46,13 +46,10 @@ Describe 'env add and remove path' -Tag 'Scoop', 'Windows' {
BeforeAll {
# test data
$manifest = @{
'env_add_path' = @('foo', 'bar')
'env_add_path' = @('foo', 'bar', '.', '..')
}
$testdir = Join-Path $PSScriptRoot 'path-test-directory'
$global = $false

# store the original path to prevent leakage of tests
$origPath = $env:PATH
}

It 'should concat the correct path' {
Expand All @@ -61,12 +58,16 @@ Describe 'env add and remove path' -Tag 'Scoop', 'Windows' {

# adding
env_add_path $manifest $testdir $global
Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like $testdir }
Should -Invoke -CommandName Add-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot }

env_rm_path $manifest $testdir $global
Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like $testdir }
Should -Invoke -CommandName Remove-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot }
}
}

Expand Down

0 comments on commit 5819b5a

Please sign in to comment.