Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(path): Isolate Scoop apps' PATH #5840

Merged
merged 20 commits into from Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -1351,6 +1422,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."
niheaven marked this conversation as resolved.
Show resolved Hide resolved
}
Add-Path -Path $path_dir -Global:$global -Force
if (get_config USE_ISOLATED_PATH) {
niheaven marked this conversation as resolved.
Show resolved Hide resolved
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