Skip to content

Commit 5819b5a

Browse files
authored
feat(path): Isolate Scoop apps' PATH (#5840)
1 parent fa06e92 commit 5819b5a

File tree

8 files changed

+150
-56
lines changed

8 files changed

+150
-56
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- **bucket:** Make official buckets higher priority ([#5398](https://github.com/ScoopInstaller/Scoop/issues/5398))
99
- **core:** Add `-Quiet` switch for `Invoke-ExternalCommand` ([#5346](https://github.com/ScoopInstaller/Scoop/issues/5346))
1010
- **core:** Allow global install of PowerShell modules ([#5611](https://github.com/ScoopInstaller/Scoop/issues/5611))
11+
- **path:** Isolate Scoop apps' PATH ([#5840](https://github.com/ScoopInstaller/Scoop/issues/5840))
1112

1213
### Bug Fixes
1314

bin/uninstall.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ if ($purge) {
100100
}
101101

102102
Remove-Path -Path (shimdir $global) -Global:$global
103+
if (get_config USE_ISOLATED_PATH) {
104+
Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
105+
}
103106

104107
success 'Scoop has been uninstalled.'

lib/core.ps1

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ function set_config {
132132
$value = [System.Convert]::ToBoolean($value)
133133
}
134134

135+
# Initialize config's change
136+
Complete-ConfigChange -Name $name -Value $value
137+
135138
if ($null -eq $scoopConfig.$name) {
136139
$scoopConfig | Add-Member -MemberType NoteProperty -Name $name -Value $value
137140
} else {
@@ -147,6 +150,74 @@ function set_config {
147150
return $scoopConfig
148151
}
149152

153+
function Complete-ConfigChange {
154+
[CmdletBinding()]
155+
param (
156+
[Parameter(Mandatory, Position = 0)]
157+
[string]
158+
$Name,
159+
[Parameter(Mandatory, Position = 1)]
160+
[AllowEmptyString()]
161+
[string]
162+
$Value
163+
)
164+
165+
if ($Name -eq 'use_isolated_path') {
166+
$oldValue = get_config USE_ISOLATED_PATH
167+
if ($Value -eq $oldValue) {
168+
return
169+
} else {
170+
$currPathEnvVar = $scoopPathEnvVar
171+
}
172+
. "$PSScriptRoot\..\lib\system.ps1"
173+
174+
if ($Value -eq $false -or $Value -eq '') {
175+
info 'Turn off Scoop isolated path... This may take a while, please wait.'
176+
$movedPath = Get-EnvVar -Name $currPathEnvVar
177+
if ($movedPath) {
178+
Add-Path -Path $movedPath -Quiet
179+
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Quiet
180+
Set-EnvVar -Name $currPathEnvVar -Quiet
181+
}
182+
if (is_admin) {
183+
$movedPath = Get-EnvVar -Name $currPathEnvVar -Global
184+
if ($movedPath) {
185+
Add-Path -Path $movedPath -Global -Quiet
186+
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Global -Quiet
187+
Set-EnvVar -Name $currPathEnvVar -Global -Quiet
188+
}
189+
}
190+
} else {
191+
$newPathEnvVar = if ($Value -eq $true) {
192+
'SCOOP_PATH'
193+
} else {
194+
$Value.ToUpperInvariant()
195+
}
196+
info "Turn on Scoop isolated path ('$newPathEnvVar')... This may take a while, please wait."
197+
$movedPath = Remove-Path -Path "$scoopdir\apps\*" -TargetEnvVar $currPathEnvVar -Quiet -PassThru
198+
if ($movedPath) {
199+
Add-Path -Path $movedPath -TargetEnvVar $newPathEnvVar -Quiet
200+
Add-Path -Path ('%' + $newPathEnvVar + '%') -Quiet
201+
if ($currPathEnvVar -ne 'PATH') {
202+
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Quiet
203+
Set-EnvVar -Name $currPathEnvVar -Quiet
204+
}
205+
}
206+
if (is_admin) {
207+
$movedPath = Remove-Path -Path "$globaldir\apps\*" -TargetEnvVar $currPathEnvVar -Global -Quiet -PassThru
208+
if ($movedPath) {
209+
Add-Path -Path $movedPath -TargetEnvVar $newPathEnvVar -Global -Quiet
210+
Add-Path -Path ('%' + $newPathEnvVar + '%') -Global -Quiet
211+
if ($currPathEnvVar -ne 'PATH') {
212+
Remove-Path -Path ('%' + $currPathEnvVar + '%') -Global -Quiet
213+
Set-EnvVar -Name $currPathEnvVar -Global -Quiet
214+
}
215+
}
216+
}
217+
}
218+
}
219+
}
220+
150221
function setup_proxy() {
151222
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
152223
$proxy = get_config PROXY
@@ -303,7 +374,7 @@ function filesize($length) {
303374
} else {
304375
if ($null -eq $length) {
305376
$length = 0
306-
}
377+
}
307378
"$($length) B"
308379
}
309380
}
@@ -1350,6 +1421,13 @@ $globaldir = $env:SCOOP_GLOBAL, (get_config GLOBAL_PATH), "$([System.Environment
13501421
# Use at your own risk.
13511422
$cachedir = $env:SCOOP_CACHE, (get_config CACHE_PATH), "$scoopdir\cache" | Where-Object { $_ } | Select-Object -First 1 | Get-AbsolutePath
13521423

1424+
# Scoop apps' PATH Environment Variable
1425+
$scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
1426+
{ $_ -is [string] } { $_.ToUpperInvariant() }
1427+
$true { 'SCOOP_PATH' }
1428+
default { 'PATH' }
1429+
}
1430+
13531431
# OS information
13541432
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
13551433

lib/install.ps1

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -906,34 +906,21 @@ function env_add_path($manifest, $dir, $global, $arch) {
906906
$env_add_path = arch_specific 'env_add_path' $manifest $arch
907907
$dir = $dir.TrimEnd('\')
908908
if ($env_add_path) {
909-
# GH-3785: Add path in ascending order.
910-
[Array]::Reverse($env_add_path)
911-
$env_add_path | Where-Object { $_ } | ForEach-Object {
912-
if ($_ -eq '.') {
913-
$path_dir = $dir
914-
} else {
915-
$path_dir = Join-Path $dir $_
916-
}
917-
if (!(is_in_dir $dir $path_dir)) {
918-
abort "Error in manifest: env_add_path '$_' is outside the app directory."
919-
}
920-
Add-Path -Path $path_dir -Global:$global -Force
909+
if (get_config USE_ISOLATED_PATH) {
910+
Add-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global
921911
}
912+
$path = $env_add_path.Where({ $_ }).ForEach({ Join-Path $dir $_ | Get-AbsolutePath }).Where({ is_in_dir $dir $_ })
913+
Add-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global -Force
922914
}
923915
}
924916

925917
function env_rm_path($manifest, $dir, $global, $arch) {
926918
$env_add_path = arch_specific 'env_add_path' $manifest $arch
927919
$dir = $dir.TrimEnd('\')
928920
if ($env_add_path) {
929-
$env_add_path | Where-Object { $_ } | ForEach-Object {
930-
if ($_ -eq '.') {
931-
$path_dir = $dir
932-
} else {
933-
$path_dir = Join-Path $dir $_
934-
}
935-
Remove-Path -Path $path_dir -Global:$global
936-
}
921+
$path = $env_add_path.Where({ $_ }).ForEach({ Join-Path $dir $_ | Get-AbsolutePath }).Where({ is_in_dir $dir $_ })
922+
Remove-Path -Path $path -Global:$global # TODO: Remove after forced isolating Scoop path
923+
Remove-Path -Path $path -TargetEnvVar $scoopPathEnvVar -Global:$global
937924
}
938925
}
939926

lib/system.ps1

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,63 +73,79 @@ function Set-EnvVar {
7373
Publish-EnvVar
7474
}
7575

76-
function Test-PathLikeEnvVar {
76+
function Split-PathLikeEnvVar {
7777
param(
78-
[string]$Name,
78+
[string[]]$Pattern,
7979
[string]$Path
8080
)
8181

8282
if ($null -eq $Path -and $Path -eq '') {
83-
return $false, $null
83+
return $null, $null
8484
} else {
85-
$strippedPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries).Where({ $_ -ne $Name }) -join ';'
86-
return ($strippedPath -ne $Path), $strippedPath
85+
$splitPattern = $Pattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)
86+
$splitPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)
87+
$inPath = @()
88+
foreach ($p in $splitPattern) {
89+
$inPath += $splitPath.Where({ $_ -like $p })
90+
$splitPath = $splitPath.Where({ $_ -notlike $p })
91+
}
92+
return ($inPath -join ';'), ($splitPath -join ';')
8793
}
8894
}
8995

9096
function Add-Path {
9197
param(
92-
[string]$Path,
98+
[string[]]$Path,
99+
[string]$TargetEnvVar = 'PATH',
93100
[switch]$Global,
94-
[switch]$Force
101+
[switch]$Force,
102+
[switch]$Quiet
95103
)
96104

97-
if (!$Path.Contains('%')) {
98-
$Path = Get-AbsolutePath $Path
99-
}
100105
# future sessions
101-
$inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global)
106+
$inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global)
102107
if (!$inPath -or $Force) {
103-
Write-Output "Adding $(friendly_path $Path) to $(if ($Global) {'global'} else {'your'}) path."
104-
Set-EnvVar -Name 'PATH' -Value (@($Path, $strippedPath) -join ';') -Global:$Global
108+
if (!$Quiet) {
109+
$Path | ForEach-Object {
110+
Write-Host "Adding $(friendly_path $_) to $(if ($Global) {'global'} else {'your'}) path."
111+
}
112+
}
113+
Set-EnvVar -Name $TargetEnvVar -Value ((@($Path) + $strippedPath) -join ';') -Global:$Global
105114
}
106115
# current session
107-
$inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH
116+
$inPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH
108117
if (!$inPath -or $Force) {
109-
$env:PATH = @($Path, $strippedPath) -join ';'
118+
$env:PATH = (@($Path) + $strippedPath) -join ';'
110119
}
111120
}
112121

113122
function Remove-Path {
114123
param(
115-
[string]$Path,
116-
[switch]$Global
124+
[string[]]$Path,
125+
[string]$TargetEnvVar = 'PATH',
126+
[switch]$Global,
127+
[switch]$Quiet,
128+
[switch]$PassThru
117129
)
118130

119-
if (!$Path.Contains('%')) {
120-
$Path = Get-AbsolutePath $Path
121-
}
122131
# future sessions
123-
$inPath, $strippedPath = Test-PathLikeEnvVar $Path (Get-EnvVar -Name 'PATH' -Global:$Global)
132+
$inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global)
124133
if ($inPath) {
125-
Write-Output "Removing $(friendly_path $Path) from $(if ($Global) {'global'} else {'your'}) path."
126-
Set-EnvVar -Name 'PATH' -Value $strippedPath -Global:$Global
134+
if (!$Quiet) {
135+
$Path | ForEach-Object {
136+
Write-Host "Removing $(friendly_path $_) from $(if ($Global) {'global'} else {'your'}) path."
137+
}
138+
}
139+
Set-EnvVar -Name $TargetEnvVar -Value $strippedPath -Global:$Global
127140
}
128141
# current session
129-
$inPath, $strippedPath = Test-PathLikeEnvVar $Path $env:PATH
130-
if ($inPath) {
142+
$inSessionPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH
143+
if ($inSessionPath) {
131144
$env:PATH = $strippedPath
132145
}
146+
if ($PassThru) {
147+
return $inPath
148+
}
133149
}
134150

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

147163
function strip_path($orig_path, $dir) {
148-
Show-DeprecatedWarning $MyInvocation 'Test-PathLikeEnvVar'
149-
Test-PathLikeEnvVar -Name $dir -Path $orig_path
164+
Show-DeprecatedWarning $MyInvocation 'Split-PathLikeEnvVar'
165+
Split-PathLikeEnvVar -Name $dir -Path $orig_path
150166
}
151167

152168
function add_first_in_path($dir, $global) {

libexec/scoop-config.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@
115115
# Nightly version is formatted as 'nightly-yyyyMMdd' and will be updated after one day if this is set to $true.
116116
# Otherwise, nightly version will not be updated unless `--force` is used.
117117
#
118+
# use_isolated_path: $true|$false|[string]
119+
# When set to $true, Scoop will use `SCOOP_PATH` environment variable to store apps' `PATH`s.
120+
# When set to arbitrary non-empty string, Scoop will use that string as the environment variable name instead.
121+
# This is useful when you want to isolate Scoop from the system `PATH`.
122+
#
118123
# ARIA2 configuration
119124
# -------------------
120125
#

libexec/scoop-reset.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ $apps | ForEach-Object {
8080
$dir = link_current $dir
8181
create_shims $manifest $dir $global $architecture
8282
create_startmenu_shortcuts $manifest $dir $global $architecture
83+
# unset all potential old env before re-adding
84+
env_rm_path $manifest $dir $global $architecture
85+
env_rm $manifest $global $architecture
8386
env_add_path $manifest $dir $global $architecture
8487
env_set $manifest $dir $global $architecture
8588
# unlink all potential old link before re-persisting

test/Scoop-Install.Tests.ps1

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,10 @@ Describe 'env add and remove path' -Tag 'Scoop', 'Windows' {
4646
BeforeAll {
4747
# test data
4848
$manifest = @{
49-
'env_add_path' = @('foo', 'bar')
49+
'env_add_path' = @('foo', 'bar', '.', '..')
5050
}
5151
$testdir = Join-Path $PSScriptRoot 'path-test-directory'
5252
$global = $false
53-
54-
# store the original path to prevent leakage of tests
55-
$origPath = $env:PATH
5653
}
5754

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

6259
# adding
6360
env_add_path $manifest $testdir $global
64-
Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
65-
Assert-MockCalled Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
61+
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
62+
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
63+
Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like $testdir }
64+
Should -Invoke -CommandName Add-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot }
6665

6766
env_rm_path $manifest $testdir $global
68-
Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
69-
Assert-MockCalled Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
67+
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" }
68+
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" }
69+
Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like $testdir }
70+
Should -Invoke -CommandName Remove-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot }
7071
}
7172
}
7273

0 commit comments

Comments
 (0)