diff --git a/DOCS/en/winux_shell_integration_en.md b/DOCS/en/winux_shell_integration_en.md index 9bdf1a0..0634086 100644 --- a/DOCS/en/winux_shell_integration_en.md +++ b/DOCS/en/winux_shell_integration_en.md @@ -27,6 +27,12 @@ To auto-activate in every new interactive PowerShell session, install the if (Get-Command winux -ErrorAction SilentlyContinue) { winux activate 6>$null } + +If `winux activate` reports `winux.ps1 not found`, the usual cause is a stale +older wrapper still left in `$PROFILE` from a previous install path such as an +old Scoop location. Re-run `winux-activate.ps1` from the current WinuxCmd +install to rewrite the wrapper against the latest `%LOCALAPPDATA%\WinuxCmd` +runtime. ``` ### CMD / Windows Terminal @@ -41,3 +47,5 @@ commands remain handled by cmd or PowerShell. - Run `winux activate`, or add the auto-activation snippet above to `$PROFILE`. 2. Profile script not loaded: - verify execution policy and profile path with `echo $PROFILE`. +3. `ls` seems to be hijacked even though `Get-Command ls` still shows the PowerShell alias: + - another tool may have installed a `PSConsoleHostReadLine` rewrite in `$PROFILE`; Microsoft `coreutils` does this, so inspect your profile for injected wrapper blocks. diff --git a/DOCS/zh/winux_shell_integration_zh.md b/DOCS/zh/winux_shell_integration_zh.md index a6a4160..a019ba5 100644 --- a/DOCS/zh/winux_shell_integration_zh.md +++ b/DOCS/zh/winux_shell_integration_zh.md @@ -25,6 +25,11 @@ wrapper,然后把下面这段放在 `$PROFILE` 里 wrapper 之后: if (Get-Command winux -ErrorAction SilentlyContinue) { winux activate 6>$null } + +如果 `winux activate` 提示 `winux.ps1 not found`,通常不是当前安装包漏文件, +而是 `$PROFILE` 里还残留着旧安装路径生成的 wrapper,例如早期 Scoop 路径。 +请从当前 WinuxCmd 安装目录重新运行 `winux-activate.ps1`,让 wrapper 改为动态 +查找最新的 `%LOCALAPPDATA%\WinuxCmd` 运行时目录。 ``` ### CMD / Windows Terminal @@ -39,3 +44,5 @@ PowerShell 自己处理。 - 运行 `winux activate`,或把上面的自动激活片段加入 `$PROFILE`。 2. Profile 没生效: - 检查执行策略与 profile 路径(`echo $PROFILE`)。 +3. 明明 `Get-Command ls` 还是 PowerShell alias,但输入 `ls` 却跑到了别的实现: + - 说明可能有别的工具在 `$PROFILE` 里注入了 `PSConsoleHostReadLine` 改写逻辑;微软 `coreutils` 就会这样做。 diff --git a/scripts/winux-activate.ps1 b/scripts/winux-activate.ps1 index 3d0bb9d..d21b7a3 100644 --- a/scripts/winux-activate.ps1 +++ b/scripts/winux-activate.ps1 @@ -26,6 +26,36 @@ function Write-Color { Write-Host "$($Markers[$Color]) $Text" } +function Get-ProfileInstallPath { + if (-not [string]::IsNullOrWhiteSpace($env:WINUXCMD_PROFILE_PATH)) { + return $env:WINUXCMD_PROFILE_PATH + } + + return $PROFILE.CurrentUserAllHosts +} + +function Remove-LegacyProfileBlocks { + param([string]$Content) + + $options = [System.Text.RegularExpressions.RegexOptions]::Singleline -bor + [System.Text.RegularExpressions.RegexOptions]::Multiline + $patterns = @( + '# WinuxCmd wrapper.*?(?=^# Find winuxcmd\.exe|\Z)', + '^# set alias\s*\r?\nUpdate-WinuxCmdAlias\s*\r?\n\r?\n', + '^function Update-WinuxCmdAlias\s*\{.*?^\}', + '^function global:winux\s*\{.*?^\}', + '^Set-Alias -Name winuxcmd -Value [^\r\n]+\r?\n?', + '^# =+[\r\n]+# WinuxCmd Integration.*?# =+[\r\n]+# End WinuxCmd Integration[\r\n]*', + '^Register-EngineEvent -SourceIdentifier PowerShell\.Exiting -Action \{.*?\} \| Out-Null\r?\n?' + ) + + foreach ($pattern in $patterns) { + $Content = [regex]::Replace($Content, $pattern, '', $options) + } + + return $Content +} + # ========== Find WinuxCmd installation ========== function Get-WinuxBinDir { # Priority 1: Check current directory (Scoop installation scenario) @@ -350,7 +380,7 @@ Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { } | Out-Null '@ - $profilePath = $PROFILE.CurrentUserAllHosts + $profilePath = Get-ProfileInstallPath # Ensure profile directory exists $profileDir = Split-Path $profilePath -Parent @@ -365,20 +395,7 @@ Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { $currentContent = $currentContent.Trim() } - # remove old configuration. - $patterns = @( - '(?s)# WinuxCmd wrapper.*?(?=^# Find winuxcmd\.exe|\Z)', - '(?s)^# set alias\s*\r?\nUpdate-WinuxCmdAlias\s*\r?\n\r?\n', - '(?s)^function Update-WinuxCmdAlias\s*\{.*?^\}', - '(?s)^function global:winux\s*\{.*?^\}', - '^Set-Alias -Name winuxcmd -Value [^\r\n]+\r?\n?', - '(?s)^# =+[\r\n]+# WinuxCmd Integration.*?# =+[\r\n]+# End WinuxCmd Integration[\r\n]+', - '(?s)^Register-EngineEvent -SourceIdentifier PowerShell\.Exiting -Action \{.*?\} \| Out-Null' - ) - - foreach ($pattern in $patterns) { - $currentContent = $currentContent -replace $pattern, '' - } + $currentContent = Remove-LegacyProfileBlocks -Content $currentContent # remove extra space $currentContent = $currentContent -replace '\n\n\n+', "`n`n" @@ -466,7 +483,7 @@ if (-not $binDir) { Write-Color "Cyan" "Found WinuxCmd at: $binDir" # Get profile path for display -$profilePath = $PROFILE.CurrentUserAllHosts +$profilePath = Get-ProfileInstallPath # Ask for confirmation Write-Host "" @@ -492,8 +509,7 @@ if (Install-WinuxToProfile -BinDir $binDir) { Write-Color "Cyan" "Next steps:" Write-Host "1. RESTART PowerShell or run: . `$PROFILE.CurrentUserAllHosts" Write-Host "2. Test with: winux" - Write-Host "3. If you have winux.ps1, copy it to WinuxCmd bin directory" - Write-Host "4. Optional: winux activate (enables bare ls/rm/cat in PowerShell)" + Write-Host "3. Optional: winux activate (enables bare ls/rm/cat/man in PowerShell)" Write-Host "" Write-Color "Cyan" "Usage after restart:" Write-Host " > winux # Show help and version info" diff --git a/scripts/winux.ps1 b/scripts/winux.ps1 index 5ac35bc..6d91fa8 100644 --- a/scripts/winux.ps1 +++ b/scripts/winux.ps1 @@ -13,187 +13,33 @@ param( $ScriptDir = $PSScriptRoot # PowerShell aliases that conflict with WinuxCmd commands -# Note: echo, cp, where are excluded because they have AllScope option and cannot be removed $ConflictedAliases = @( "cat", "clear", "diff", "group", "kill", "ls", "mv", "ps", "pwd", "rm", "rmdir", "sleep", "sort", "tee", "type", "man" ) -# Aliases intentionally left to PowerShell because overriding them is surprising +# Aliases intentionally left to PowerShell in the default activation flow +# because overriding them is still surprising in many PowerShell sessions. $AllScopeAliases = @("echo", "cp", "where") -$CommandMap = @{ - "ls" = "ls.exe" - "cat" = "cat.exe" - "cp" = "cp.exe" - "mv" = "mv.exe" - "rm" = "rm.exe" - "mkdir" = "mkdir.exe" - "rmdir" = "rmdir.exe" - "touch" = "touch.exe" - "echo" = "echo.exe" - "head" = "head.exe" - "tail" = "tail.exe" - "find" = "find.exe" - "grep" = "grep.exe" - "sort" = "sort.exe" - "uniq" = "uniq.exe" - "cut" = "cut.exe" - "wc" = "wc.exe" - "which" = "which.exe" - "env" = "env.exe" - "sed" = "sed.exe" - "chgrp" = "chgrp.exe" - "chmod" = "chmod.exe" - "date" = "date.exe" - "diff" = "diff.exe" - "dir" = "dir.exe" - "vdir" = "vdir.exe" - "dircolors" = "dircolors.exe" - "ln" = "ln.exe" - "ps" = "ps.exe" - "pwd" = "pwd.exe" - "tee" = "tee.exe" - "xargs" = "xargs.exe" - "file" = "file.exe" - "realpath" = "realpath.exe" - "df" = "df.exe" - "du" = "du.exe" - "kill" = "kill.exe" - "tree" = "tree.exe" - "lsof" = "lsof.exe" - # New commands added in v0.5.3 - "base64" = "base64.exe" - "tr" = "tr.exe" - "less" = "less.exe" - "watch" = "watch.exe" - "jq" = "jq.exe" - "md5sum" = "md5sum.exe" - "sha256sum" = "sha256sum.exe" - "basename" = "basename.exe" - "dirname" = "dirname.exe" - "free" = "free.exe" - "column" = "column.exe" - "col" = "col.exe" - "seq" = "seq.exe" - "stat" = "stat.exe" - # New commands added in v0.7.0 - Hash tools - "sha1sum" = "sha1sum.exe" - "sha224sum" = "sha224sum.exe" - "sha384sum" = "sha384sum.exe" - "sha512sum" = "sha512sum.exe" - "b2sum" = "b2sum.exe" - # New commands added in v0.7.0 - Text processing - "paste" = "paste.exe" - "join" = "join.exe" - "comm" = "comm.exe" - "split" = "split.exe" - "csplit" = "csplit.exe" - "cmp" = "cmp.exe" - "nl" = "nl.exe" - "fold" = "fold.exe" - "fmt" = "fmt.exe" - "more" = "more.exe" - # New commands added in v0.7.0 - Text conversion - "expand" = "expand.exe" - "unexpand" = "unexpand.exe" - "tac" = "tac.exe" - # New commands added in v0.7.0 - System information - "hostname" = "hostname.exe" - "whoami" = "whoami.exe" - "arch" = "arch.exe" - "uname" = "uname.exe" - "id" = "id.exe" - "who" = "who.exe" - "users" = "users.exe" - "groups" = "groups.exe" - # New commands added in v0.7.0 - File operations - "truncate" = "truncate.exe" - "mktemp" = "mktemp.exe" - "install" = "install.exe" - "readlink" = "readlink.exe" - "cksum" = "cksum.exe" - "sum" = "sum.exe" - # New commands added in v0.7.0 - Other tools - "sleep" = "sleep.exe" - "timeout" = "timeout.exe" - "uptime" = "uptime.exe" - "shuf" = "shuf.exe" - "pr" = "pr.exe" - "yes" = "yes.exe" - "ptx" = "ptx.exe" - # New commands added in v0.7.0 - Basic utilities - "clear" = "clear.exe" - "true" = "true.exe" - "false" = "false.exe" - "tty" = "tty.exe" - "sync" = "sync.exe" - "reset" = "reset.exe" - "logname" = "logname.exe" - "printenv" = "printenv.exe" - # New commands added in v0.7.0 - Text processing - "rev" = "rev.exe" - "d2u" = "d2u.exe" - "u2d" = "u2d.exe" - "dos2unix" = "dos2unix.exe" - "unix2dos" = "unix2dos.exe" - "base32" = "base32.exe" - "basenc" = "basenc.exe" - "cygpath" = "cygpath.exe" - "pathchk" = "pathchk.exe" - # New commands added in v0.7.0 - Programming tools - "printf" = "printf.exe" - "expr" = "expr.exe" - "test" = "test.exe" - "[" = "[.exe" - # New commands added in v0.7.0 - Binary tools - "od" = "od.exe" - "xxd" = "xxd.exe" - "dd" = "dd.exe" - "shred" = "shred.exe" - # New commands added in v0.7.0 - System utilities - "numfmt" = "numfmt.exe" - "hmac256" = "hmac256.exe" - "nice" = "nice.exe" - "nohup" = "nohup.exe" - "stdbuf" = "stdbuf.exe" - # New commands added in v0.7.0 - Development tools - "patch" = "patch.exe" - "diff3" = "diff3.exe" - "sdiff" = "sdiff.exe" - # New commands added in v0.7.0 - Calendar and sorting - "cal" = "cal.exe" - "tsort" = "tsort.exe" - # New commands added in v0.7.0 - Terminal tools - "tput" = "tput.exe" - "infocmp" = "infocmp.exe" - "tic" = "tic.exe" - "toe" = "toe.exe" - "top" = "top.exe" - # New commands added in v0.7.0 - System information - "hostid" = "hostid.exe" - "locale" = "locale.exe" - "tzset" = "tzset.exe" - "pinky" = "pinky.exe" - "mpicalc" = "mpicalc.exe" - "strings" = "strings.exe" - "stty" = "stty.exe" - "hexdump" = "hexdump.exe" - # New commands added in v0.7.0 - Archive tools - "cpio" = "cpio.exe" - # New commands added in v0.7.0 - System utilities - "nproc" = "nproc.exe" - "getconf" = "getconf.exe" - "link" = "link.exe" - "unlink" = "unlink.exe" - "factor" = "factor.exe" - # New commands added in v0.9.0 - Coreutils - "man" = "man.exe" - "chown" = "chown.exe" -} - # ========== Functions ========== +function Get-AvailableCommands { + $commands = [ordered]@{} + $ignoredNames = @("winuxcmd") + + foreach ($exe in Get-ChildItem -LiteralPath $ScriptDir -Filter "*.exe" -File -ErrorAction SilentlyContinue) { + $commandName = $exe.BaseName + if ($commandName -in $ignoredNames) { + continue + } + + $commands[$commandName] = $exe.Name + } + + return $commands +} + function Save-ConflictedAliases { $global:Winux_SavedAliases = @{} @@ -209,10 +55,12 @@ function Save-ConflictedAliases { } function Set-WinuxAliases { + $commandMap = Get-AvailableCommands + foreach ($aliasName in $ConflictedAliases) { - if (-not $CommandMap.ContainsKey($aliasName)) { continue } + if (-not $commandMap.Contains($aliasName)) { continue } - $commandPath = Join-Path $ScriptDir $CommandMap[$aliasName] + $commandPath = Join-Path $ScriptDir $commandMap[$aliasName] if (-not (Test-Path $commandPath)) { continue } $alias = Get-Alias -Name $aliasName -ErrorAction SilentlyContinue @@ -254,20 +102,24 @@ Remove-Variable Winux_SavedAliases -Scope Global -ErrorAction SilentlyContinue function Show-CommandList { + $commandMap = Get-AvailableCommands + Write-Host "Available WinuxCmd Commands:" -ForegroundColor Cyan Write-Host "============================" -ForegroundColor Cyan - foreach ($cmd in $CommandMap.Keys | Sort-Object) { + foreach ($cmd in $commandMap.Keys | Sort-Object) { Write-Host " $cmd" -ForegroundColor Yellow -NoNewline - Write-Host " -> $($CommandMap[$cmd])" + Write-Host " -> $($commandMap[$cmd])" } Write-Host "" - Write-Host "Total: $($CommandMap.Count) commands" -ForegroundColor Yellow + Write-Host "Total: $($commandMap.Count) commands" -ForegroundColor Yellow Write-Host "To use these commands, run: .\winux.ps1 activate" -ForegroundColor Green } function Show-Status { + $commandMap = Get-AvailableCommands + Write-Host "WinuxCmd Status:" -ForegroundColor Cyan Write-Host "================" -ForegroundColor Cyan @@ -277,7 +129,7 @@ function Show-Status { if ($winuxInPath) { Write-Host "Status: ACTIVE" -ForegroundColor Green Write-Host "Directory: $ScriptDir" -ForegroundColor Gray - Write-Host "Commands: $($CommandMap.Count) available" -ForegroundColor Yellow + Write-Host "Commands: $($commandMap.Count) available" -ForegroundColor Yellow } else { Write-Host "Status: INACTIVE" -ForegroundColor Gray @@ -289,6 +141,8 @@ function Show-Status { } function Invoke-Activate { + $commandMap = Get-AvailableCommands + Write-Host "Activating WinuxCmd..." -ForegroundColor Green Save-ConflictedAliases @@ -301,15 +155,15 @@ function Invoke-Activate { Write-Host "Activation complete!" -ForegroundColor Green Write-Host "" - Write-Host "Total: $($CommandMap.Count) commands" -ForegroundColor Yellow + Write-Host "Total: $($commandMap.Count) commands" -ForegroundColor Yellow Write-Host "Directory: $ScriptDir" -ForegroundColor Gray Write-Host "" - # Show note for aliases intentionally left to PowerShell + # Show note for aliases intentionally left to PowerShell in the default flow. if ($AllScopeAliases.Count -gt 0) { - Write-Host "Note: The following command names are kept as PowerShell aliases:" -ForegroundColor Yellow + Write-Host "Note: The default activation flow leaves these PowerShell aliases unchanged:" -ForegroundColor Yellow Write-Host " $($AllScopeAliases -join ', ')" -ForegroundColor Magenta - Write-Host " Use `.exe` extension to run WinuxCmd version, e.g.:" -ForegroundColor Cyan + Write-Host " Use the WinuxCmd executable name when needed, for example:" -ForegroundColor Cyan Write-Host " echo.exe, cp.exe, where.exe" -ForegroundColor Green Write-Host "" } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3af0dca..1652976 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -143,6 +143,20 @@ add_custom_target(create-test-links # Make sure links are created before running tests add_dependencies(winuxcmd-tests create-test-links) +add_test( + NAME winux_activate_profile_cleanup + COMMAND ${POWERSHELL_EXE} -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/winux_activate_profile_cleanup_test.ps1" +) +set_tests_properties(winux_activate_profile_cleanup PROPERTIES LABELS "WinuxCmd;PowerShell") + +add_test( + NAME winux_command_discovery + COMMAND ${POWERSHELL_EXE} -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/winux_command_discovery_test.ps1" +) +set_tests_properties(winux_command_discovery PROPERTIES LABELS "WinuxCmd;PowerShell") + # Also add dependency to test-all target if(TARGET test-all) add_dependencies(test-all create-test-links) @@ -159,4 +173,4 @@ foreach(src ${ALL_TEST_SOURCES}) add_dependencies(test-${group_name} create-test-links) endif() endif() -endforeach() \ No newline at end of file +endforeach() diff --git a/tests/scripts/winux_activate_profile_cleanup_test.ps1 b/tests/scripts/winux_activate_profile_cleanup_test.ps1 new file mode 100644 index 0000000..21a1a8f --- /dev/null +++ b/tests/scripts/winux_activate_profile_cleanup_test.ps1 @@ -0,0 +1,57 @@ +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$sourceScript = Join-Path $repoRoot "scripts\winux-activate.ps1" + +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("winux-activate-profile-test-" + [guid]::NewGuid().ToString("N")) +$null = New-Item -ItemType Directory -Path $tempRoot -Force + +try { + $installRoot = Join-Path $tempRoot "install" + $null = New-Item -ItemType Directory -Path $installRoot -Force + + Copy-Item -LiteralPath $sourceScript -Destination (Join-Path $installRoot "winux-activate.ps1") + Copy-Item -LiteralPath $env:ComSpec -Destination (Join-Path $installRoot "winuxcmd.exe") + + $profilePath = Join-Path $tempRoot "profile.ps1" + @' +function global:winux { + param( + [Parameter(Position=0,ValueFromRemainingArguments=$true)] + [string[]]$Arguments + ) + $winuxPs1 = Join-Path $env:USERPROFILE\scoop\apps\winuxcmd\current 'winux.ps1' + if (Test-Path $winuxPs1) { + & $winuxPs1 -Action activate + } else { + Write-Host 'winux.ps1 not found' + } +} +'@ | Set-Content -LiteralPath $profilePath -Encoding UTF8 + + $env:WINUXCMD_PROFILE_PATH = $profilePath + $command = @" +Set-Location '$installRoot' +'Y' | & '$installRoot\winux-activate.ps1' +"@ + + powershell -NoProfile -ExecutionPolicy Bypass -Command $command | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "winux-activate.ps1 exited with $LASTEXITCODE" + } + + $profileContent = Get-Content -LiteralPath $profilePath -Raw + if ($profileContent -match 'scoop\\apps\\winuxcmd\\current') { + throw "stale Scoop wrapper was not removed" + } + if (($profileContent | Select-String -Pattern 'function global:winux' -AllMatches).Matches.Count -ne 1) { + throw "expected exactly one winux wrapper after rewrite" + } + if ($profileContent -notmatch '\$env:LOCALAPPDATA\\WinuxCmd') { + throw "expected dynamic LOCALAPPDATA-based wrapper" + } +} +finally { + Remove-Item -LiteralPath Env:WINUXCMD_PROFILE_PATH -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/tests/scripts/winux_command_discovery_test.ps1 b/tests/scripts/winux_command_discovery_test.ps1 new file mode 100644 index 0000000..7a829d0 --- /dev/null +++ b/tests/scripts/winux_command_discovery_test.ps1 @@ -0,0 +1,59 @@ +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$sourceScript = Join-Path $repoRoot "scripts\winux.ps1" + +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("winux-command-discovery-" + [guid]::NewGuid().ToString("N")) +$null = New-Item -ItemType Directory -Path $tempRoot -Force + +try { + Copy-Item -LiteralPath $sourceScript -Destination (Join-Path $tempRoot "winux.ps1") + + foreach ($name in @("ls.exe", "cat.exe", "man.exe", "winuxcmd.exe")) { + Copy-Item -LiteralPath $env:ComSpec -Destination (Join-Path $tempRoot $name) + } + + $listOutput = powershell -NoProfile -ExecutionPolicy Bypass -Command @" +Set-Location '$tempRoot' +& '$tempRoot\winux.ps1' -Action list 6>&1 +"@ + + if ($LASTEXITCODE -ne 0) { + throw "winux.ps1 list exited with $LASTEXITCODE" + } + + $listText = ($listOutput | Out-String) + + if ($listText -notmatch 'ls\s*\r?\n\s*-> ls\.exe') { + throw "expected ls.exe in command list" + } + if ($listText -notmatch 'cat\s*\r?\n\s*-> cat\.exe') { + throw "expected cat.exe in command list" + } + if ($listText -notmatch 'man\s*\r?\n\s*-> man\.exe') { + throw "expected man.exe in command list" + } + if ($listText -match 'winuxcmd\s*\r?\n\s*-> winuxcmd\.exe') { + throw "winuxcmd.exe should not be listed as a command alias target" + } + if ($listText -notmatch 'Total: 3 commands') { + throw "expected dynamic command total based on discovered executables" + } + + powershell -NoProfile -ExecutionPolicy Bypass -Command @" +Set-Location '$tempRoot' +& '$tempRoot\winux.ps1' -Action activate | Out-Null +`$ls = Get-Alias ls -ErrorAction Stop +if (`$ls.Definition -ne (Join-Path '$tempRoot' 'ls.exe')) { exit 21 } +`$man = Get-Alias man -ErrorAction Stop +if (`$man.Definition -ne (Join-Path '$tempRoot' 'man.exe')) { exit 22 } +exit 0 +"@ | Out-Null + + if ($LASTEXITCODE -ne 0) { + throw "winux.ps1 activate alias verification failed with $LASTEXITCODE" + } +} +finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +}