diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..91df8eba1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +--- +# ~/.github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # == /.github/workflows/ + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e87fa1b2e..3c7541f7bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 2 - name: Init Test Suite - uses: potatoqualitee/psmodulecache@v5.1 + uses: potatoqualitee/psmodulecache@main with: modules-to-cache: BuildHelpers shell: powershell @@ -30,7 +30,7 @@ jobs: with: fetch-depth: 2 - name: Init Test Suite - uses: potatoqualitee/psmodulecache@v5.1 + uses: potatoqualitee/psmodulecache@main with: modules-to-cache: BuildHelpers shell: pwsh diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae5f38000..00e336583e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,94 @@ +## [v0.4.0](https://github.com/ScoopInstaller/Scoop/compare/v0.3.1...v0.4.0) - 2024-04-18 + +### Features + +- **scoop-update:** Add support for parallel syncing buckets in PowerShell 7 and improve output ([#5122](https://github.com/ScoopInstaller/Scoop/issues/5122)) +- **bucket:** Switch nirsoft bucket to ScoopInstaller/Nirsoft ([#5328](https://github.com/ScoopInstaller/Scoop/issues/5328)) +- **bucket:** Make official buckets higher priority ([#5398](https://github.com/ScoopInstaller/Scoop/issues/5398)) +- **config:** Support portable config file ([#5369](https://github.com/ScoopInstaller/Scoop/issues/5369)) +- **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 + +- **scoop-alias:** Prevent overwrite existing file when adding alias ([#5577](https://github.com/ScoopInstaller/Scoop/issues/5577)) +- **scoop-checkup:** Skip defender check in Windows Sandbox ([#5519](https://github.com/ScoopInstaller/Scoop/issues/5519)) +- **scoop-checkup:** Change the message level of helpers from ERROR to WARN ([#5614](https://github.com/ScoopInstaller/Scoop/issues/5614)) +- **scoop-checkup:** Don't throw 7zip error when external 7zip is used ([#5703](https://github.com/ScoopInstaller/Scoop/issues/5703)) +- **scoop-(un)hold:** Correct output the messages when manifest not found, (already|not) held ([#5519](https://github.com/ScoopInstaller/Scoop/issues/5519)) +- **scoop-info:** Fix errors in file size collection when `--verbose` ([#5352](https://github.com/ScoopInstaller/Scoop/issues/5352)) +- **scoop-reset:** Don't abort when multiple apps are passed and an app is running ([#5687](https://github.com/ScoopInstaller/Scoop/issues/5687)) +- **scoop-update:** Change error message to a better instruction ([#5677](https://github.com/ScoopInstaller/Scoop/issues/5677)) +- **scoop-virustotal:** Fix `scoop-virustotal` when `--all` has been passed without app ([#5593](https://github.com/ScoopInstaller/Scoop/issues/5593)) +- **scoop-virustotal:** Fix the issue that escape character not available in PowerShell 5.1 ([#5870](https://github.com/ScoopInstaller/Scoop/issues/5870)) +- **autoupdate:** Fix file hash extraction ([#5295](https://github.com/ScoopInstaller/Scoop/issues/5295)) +- **autoupdate:** Fix bug that 'WebClient' doesn't auto-extract 'gzip' ([#5901](https://github.com/ScoopInstaller/Scoop/issues/5901)) +- **buckets:** Avoid error messages for unexpected dir ([#5549](https://github.com/ScoopInstaller/Scoop/issues/5549)) +- **config:** Warn users about misconfigured GitHub token ([#5777](https://github.com/ScoopInstaller/Scoop/issues/5777)) +- **core:** Fix scripts' calling parameters ([#5365](https://github.com/ScoopInstaller/Scoop/issues/5365)) +- **core:** Fix `is_in_dir` under Unix ([#5391](https://github.com/ScoopInstaller/Scoop/issues/5391)) +- **core:** Rewrite config file when needed ([#5439](https://github.com/ScoopInstaller/Scoop/issues/5439)) +- **core:** Prevents leaking HTTP(S)_PROXY env vars to current sessions after Invoke-Git in parallel execution ([#5436](https://github.com/ScoopInstaller/Scoop/issues/5436)) +- **core:** Handle scoop aliases and broken(edited,copied) shim ([#5551](https://github.com/ScoopInstaller/Scoop/issues/5551)) +- **core:** Avoid error messages when deleting non-existent environment variable ([#5547](https://github.com/ScoopInstaller/Scoop/issues/5547)) +- **core:** Use relative path as fallback of `$scoopdir` ([#5544](https://github.com/ScoopInstaller/Scoop/issues/5544)) +- **core:** Fix detection of Git ([#5545](https://github.com/ScoopInstaller/Scoop/issues/5545)) +- **core:** Do not call `scoop` externally from inside the code ([#5695](https://github.com/ScoopInstaller/Scoop/issues/5695)) +- **core:** Fix arguments parsing method of `Invoke-ExternalCommand()` ([#5839](https://github.com/ScoopInstaller/Scoop/issues/5839)) +- **decompress:** Exclude '*.nsis' that may cause error ([#5294](https://github.com/ScoopInstaller/Scoop/issues/5294)) +- **decompress:** Remove unused parent dir w/ 'extract_dir' ([#5682](https://github.com/ScoopInstaller/Scoop/issues/5682)) +- **decompress:** Use `wix.exe` in WiX Toolset v4+ as primary extractor of `Expand-DarkArchive()` ([#5871](https://github.com/ScoopInstaller/Scoop/issues/5871)) +- **env:** Avoid automatic expansion of `%%` in env ([#5395](https://github.com/ScoopInstaller/Scoop/issues/5395), [#5452](https://github.com/ScoopInstaller/Scoop/issues/5452), [#5631](https://github.com/ScoopInstaller/Scoop/issues/5631)) +- **getopt:** Stop split arguments in `getopt()` and ensure array by explicit arguments type ([#5326](https://github.com/ScoopInstaller/Scoop/issues/5326)) +- **install:** Fix download from private GitHub repositories ([#5361](https://github.com/ScoopInstaller/Scoop/issues/5361)) +- **install:** Avoid error when unlinking non-existent junction/hardlink ([#5552](https://github.com/ScoopInstaller/Scoop/issues/5552)) +- **manifest:** Correct source of manifest ([#5575](https://github.com/ScoopInstaller/Scoop/issues/5575)) +- **shim:** Remove console window for GUI applications ([#5559](https://github.com/ScoopInstaller/Scoop/issues/5559)) +- **shim:** Use bash executable directly ([#5433](https://github.com/ScoopInstaller/Scoop/issues/5433)) +- **shim:** Check literal path in `Get-ShimPath` ([#5680](https://github.com/ScoopInstaller/Scoop/issues/5680)) +- **shim:** Avoid unexpected output of `list` subcommand ([#5681](https://github.com/ScoopInstaller/Scoop/issues/5681)) +- **shim:** Allow GUI applications to attach to the shell's console when launched using the GUI shim ([#5721](https://github.com/ScoopInstaller/Scoop/issues/5721)) +- **shim:** Run JAR file from app's root directory ([#5872](https://github.com/ScoopInstaller/Scoop/issues/5872)) +- **shortcuts:** Output correctly formatted path ([#5333](https://github.com/ScoopInstaller/Scoop/issues/5333)) +- **update/uninstall:** Remove items from PATH correctly ([#5833](https://github.com/ScoopInstaller/Scoop/issues/5833)) + +### Performance Improvements + +- **scoop-search:** Improve performance for local search ([#5644](https://github.com/ScoopInstaller/Scoop/issues/5644)) +- **scoop-update:** Check for running process before wasting time on download ([#5799](https://github.com/ScoopInstaller/Scoop/issues/5799)) +- **decompress:** Disable progress bar to improve `Expand-Archive` performance ([#5410](https://github.com/ScoopInstaller/Scoop/issues/5410)) +- **shim:** Update kiennq-shim to v3.1.1 ([#5841](https://github.com/ScoopInstaller/Scoop/issues/5841), [#5847](https://github.com/ScoopInstaller/Scoop/issues/5847)) + +### Code Refactoring + +- **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), [#5858](https://github.com/ScoopInstaller/Scoop/issues/5858), [#5864](https://github.com/ScoopInstaller/Scoop/issues/5864)) +- **core:** Get rid of 'fullpath' ([#3533](https://github.com/ScoopInstaller/Scoop/issues/3533)) +- **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)) +- **helper:** Remove 7zip's fallback '7zip-zstd' ([#5548](https://github.com/ScoopInstaller/Scoop/issues/5548)) +- **shim:** Remove CS shim codebase ([#5903](https://github.com/ScoopInstaller/Scoop/issues/5903)) + +### Builds + +- **checkver:** Read the private_host config variable ([#5381](https://github.com/ScoopInstaller/Scoop/issues/5381)) +- **supporting:** Update Json to 13.0.3, Json.Schema to 3.0.15 ([#5835](https://github.com/ScoopInstaller/Scoop/issues/5835)) + +### Continuous Integration + +- **dependabot:** Add dependabot.yml for GitHub Actions ([#5377](https://github.com/ScoopInstaller/Scoop/issues/5377)) +- **module:** Update 'psmodulecache' version to 'main' ([#5828](https://github.com/ScoopInstaller/Scoop/issues/5828)) + +### Tests + +- **bucket:** Skip manifest validation if no manifest changes ([#5270](https://github.com/ScoopInstaller/Scoop/issues/5270)) + +### Documentation + +- **scoop-info:** Fix help message([#5445](https://github.com/ScoopInstaller/Scoop/issues/5445)) +- **readme:** Improve documentation language ([#5638](https://github.com/ScoopInstaller/Scoop/issues/5638)) + ## [v0.3.1](https://github.com/ScoopInstaller/Scoop/compare/v0.3.0...v0.3.1) - 2022-11-15 ### Features @@ -14,6 +105,7 @@ - **shim:** Exit if shim creating failed 'cause no git ([#5225](https://github.com/ScoopInstaller/Scoop/issues/5225)) - **scoop-import:** Add correct architecture argument ([#5210](https://github.com/ScoopInstaller/Scoop/issues/5210)) - **scoop-config:** Output `[DateTime]` as `[String]` ([#5232](https://github.com/ScoopInstaller/Scoop/issues/5232)) +- **shim:** fixed shim add bug related to Resolve-Path ([#5492](https://github.com/ScoopInstaller/Scoop/issues/5492)) ### Code Refactoring diff --git a/README.md b/README.md index 9895584498..641c0f6d76 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -

+

Scoop

+ -

Scoop

-

-Features -| -Installation -| -Documentation + Features + | + Installation + | + Documentation

-- - - -

+--- + +

Code Size @@ -36,43 +36,48 @@ Scoop is a command-line installer for Windows. ## What does Scoop do? -Scoop installs programs from the command line with a minimal amount of friction. It: +Scoop installs apps from the command line with a minimal amount of friction. It: -- Eliminates permission popup windows -- Hides GUI wizard-style installers -- Prevents PATH pollution from installing lots of programs -- Avoids unexpected side-effects from installing and uninstalling programs -- Finds and installs dependencies automatically -- Performs all the extra setup steps itself to get a working program +- Eliminates [User Account Control](https://learn.microsoft.com/windows/security/application-security/application-control/user-account-control/) (UAC) prompt notifications. +- Hides the graphical user interface (GUI) of wizard-style installers. +- Prevents polluting the `PATH` environment variable. Normally, this variable gets cluttered as different apps are installed on the device. +- Avoids unexpected side effects from installing and uninstalling apps. +- Resolves and installs dependencies automatically. +- Performs all the necessary steps to get an app to a working state. -Scoop is very scriptable, so you can run repeatable setups to get your environment just the way you like, e.g.: +Scoop is quite script-friendly. Your environment can become the way you like by using repeatable setups. For example: -```powershell +```console scoop install sudo sudo scoop install 7zip git openssh --global scoop install aria2 curl grep sed less touch scoop install python ruby go perl ``` -If you've built software that you'd like others to use, Scoop is an alternative to building an installer (e.g. MSI or InnoSetup) — you just need to zip your program and provide a JSON manifest that describes how to install it. +If you have built software that you would like others to use, Scoop is an alternative to building an installer (like MSI or InnoSetup). You just need to compress your app to a `.zip` file and provide a JSON manifest that describes how to install it. ## Installation -Run the following command from a **non-admin** PowerShell to install scoop to its default location `C:\Users\\scoop`. +Run the following commands from a regular (non-admin) PowerShell terminal to install Scoop: ```powershell -iwr -useb get.scoop.sh | iex +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression ``` -Advanced installation instruction and full documentation of the installer are available in [ScoopInstaller/Install](https://github.com/ScoopInstaller/Install). Please create new issues there if you have questions about the installation. +**Note**: The first command makes your device allow running the installation and management scripts. This is necessary because Windows 10 client devices restrict execution of any PowerShell scripts by default. -## [Documentation](https://github.com/ScoopInstaller/Scoop/wiki) +It will install Scoop to its default location: + +`C:\Users\\scoop` + +You can find the complete documentation about the installer, including advanced installation configurations, in [ScoopInstaller/Install](https://github.com/ScoopInstaller/Install). Please create new issues there if you have questions about the installation. ## Multi-connection downloads with `aria2` Scoop can utilize [`aria2`](https://github.com/aria2/aria2) to use multi-connection downloads. Simply install `aria2` through Scoop and it will be used for all downloads afterward. -```powershell +```console scoop install aria2 ``` @@ -90,54 +95,54 @@ You can tweak the following `aria2` settings with the `scoop config` command: ## Inspiration -- [Homebrew](http://mxcl.github.io/homebrew/) -- [sub](https://github.com/37signals/sub#readme) +- [Homebrew](https://brew.sh/) +- [Sub](https://signalvnoise.com/posts/3264-automating-with-convention-introducing-sub) ## What sort of apps can Scoop install? -The apps that install best with Scoop are commonly called "portable" apps: i.e. compressed program files that run stand-alone when extracted and don't have side-effects like changing the registry or putting files outside the program directory. - -Since installers are common, Scoop supports them too (and their uninstallers). +The apps that are most likely to get installed fine with Scoop are those referred to as "portable" apps. These apps are compressed files which can run standalone after being extracted. This type of apps does not produce side effects like changing the Windows Registry or placing files outside the app directory. -Scoop is also great at handling single-file programs and Powershell scripts. These don't even need to be compressed. See the [runat](https://github.com/ScoopInstaller/Main/blob/master/bucket/runat.json) package for an example: it's really just a GitHub gist. +Scoop also supports installer files and their uninstallation methods. Likewise, it can handle single-file apps and PowerShell scripts. These do not even need to be compressed. See the [runat](https://github.com/ScoopInstaller/Main/blob/master/bucket/runat.json) package for an example: it is simply a GitHub gist. ### Contribute to this project -If you'd like to improve Scoop by adding features or fixing bugs, please read our [Contributing Guide](https://github.com/ScoopInstaller/.github/blob/main/.github/CONTRIBUTING.md). +If you would like to improve Scoop by adding features or fixing bugs, please read our [Contributing Guide](https://github.com/ScoopInstaller/.github/blob/main/.github/CONTRIBUTING.md). ### Support this project -If you find Scoop useful and would like to support ongoing development and maintenance, here's how: +If you find Scoop useful and would like to support the ongoing development and maintenance of this project, you can donate here: -- [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DM2SUH9EUXSKJ) (one-time donation) +- [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DM2SUH9EUXSKJ) (one-time donations) ## Known application buckets -The following buckets are known to scoop: +The following buckets are known to Scoop: -- [main](https://github.com/ScoopInstaller/Main) - Default bucket for the most common (mostly CLI) apps -- [extras](https://github.com/ScoopInstaller/Extras) - Apps that don't fit the main bucket's [criteria](https://github.com/ScoopInstaller/Scoop/wiki/Criteria-for-including-apps-in-the-main-bucket) -- [games](https://github.com/Calinou/scoop-games) - Open source/freeware games and game-related tools -- [nerd-fonts](https://github.com/matthewjberger/scoop-nerd-fonts) - Nerd Fonts -- [nirsoft](https://github.com/kodybrown/scoop-nirsoft) - Almost all of the [250+](https://rasa.github.io/scoop-directory/by-apps#kodybrown_scoop-nirsoft) apps from [Nirsoft](https://nirsoft.net) -- [sysinternals](https://github.com/niheaven/scoop-sysinternals) - Sysinternals Suite and all individual application from [Microsoft](https://learn.microsoft.com/sysinternals/) -- [java](https://github.com/ScoopInstaller/Java) - A collection of Java development kits (JDKs), Java runtime engines (JREs), Java's virtual machine debugging tools and Java based runtime engines. -- [nonportable](https://github.com/ScoopInstaller/Nonportable) - Non-portable apps (may require UAC) -- [php](https://github.com/ScoopInstaller/PHP) - Installers for most versions of PHP -- [versions](https://github.com/ScoopInstaller/Versions) - Alternative versions of apps found in other buckets +- [main](https://github.com/ScoopInstaller/Main) - Default bucket which contains popular non-GUI apps. +- [extras](https://github.com/ScoopInstaller/Extras) - Apps that do not fit the main bucket's [criteria](https://github.com/ScoopInstaller/Scoop/wiki/Criteria-for-including-apps-in-the-main-bucket). +- [games](https://github.com/Calinou/scoop-games) - Open-source and freeware video games and game-related tools. +- [nerd-fonts](https://github.com/matthewjberger/scoop-nerd-fonts) - Nerd Fonts. +- [nirsoft](https://github.com/ScoopInstaller/Nirsoft) - A collection of over 250+ apps from [Nirsoft](https://nirsoft.net). +- [sysinternals](https://github.com/niheaven/scoop-sysinternals) - The Sysinternals suite from [Microsoft](https://learn.microsoft.com/sysinternals/). +- [java](https://github.com/ScoopInstaller/Java) - A collection of Java development kits (JDKs) and Java runtime engines (JREs), Java's virtual machine debugging tools and Java based runtime engines. +- [nonportable](https://github.com/ScoopInstaller/Nonportable) - Non-portable apps (may trigger UAC prompts). +- [php](https://github.com/ScoopInstaller/PHP) - Installers for most versions of PHP. +- [versions](https://github.com/ScoopInstaller/Versions) - Alternative versions of apps found in other buckets. -The main bucket is installed by default. To add any of the other buckets, type: +The `main` bucket is installed by default. You can make use of more buckets by typing: ```console -scoop bucket add bucketname +scoop bucket add ``` -For example, to add the extras bucket, type: +For example, to add the `extras` bucket, type: ```console scoop bucket add extras ``` +You would be able to install apps from the `extras` bucket now. + ## Other application buckets -Many other application buckets hosted on Github can be found in the [Scoop Directory](https://rasa.github.io/scoop-directory/) or via [other search engines](https://rasa.github.io/scoop-directory/#other-search-engines). +Many other application buckets hosted on GitHub can be found on [ScoopSearch](https://scoop.sh/) or via [other search engines](https://rasa.github.io/scoop-directory/#other-search-engines). diff --git a/bin/checkhashes.ps1 b/bin/checkhashes.ps1 index 0420ee1fa8..cbfb0af40f 100644 --- a/bin/checkhashes.ps1 +++ b/bin/checkhashes.ps1 @@ -121,7 +121,7 @@ foreach ($current in $MANIFESTS) { Invoke-CachedDownload $current.app $version $_ $null $null -use_cache:$UseCache - $to_check = fullpath (cache_path $current.app $version $_) + $to_check = cache_path $current.app $version $_ $actual_hash = (Get-FileHash -Path $to_check -Algorithm $algorithm).Hash.ToLower() # Append type of algorithm to both expected and actual if it's not sha256 @@ -146,7 +146,7 @@ foreach ($current in $MANIFESTS) { Write-Host "$($current.app): " -NoNewline Write-Host 'Mismatch found ' -ForegroundColor Red $mismatched | ForEach-Object { - $file = fullpath (cache_path $current.app $version $current.urls[$_]) + $file = cache_path $current.app $version $current.urls[$_] Write-Host "`tURL:`t`t$($current.urls[$_])" if (Test-Path $file) { Write-Host "`tFirst bytes:`t$((get_magic_bytes_pretty $file ' ').ToUpper())" diff --git a/bin/checkver.ps1 b/bin/checkver.ps1 index 56b8c8c987..07d954c671 100644 --- a/bin/checkver.ps1 +++ b/bin/checkver.ps1 @@ -226,15 +226,21 @@ $Queue | ForEach-Object { $url = substitute $url $substitutions $state = New-Object psobject @{ - app = $name; - file = $file; - url = $url; - regex = $regex; - json = $json; - jsonpath = $jsonpath; - xpath = $xpath; - reverse = $reverse; - replace = $replace; + app = $name + file = $file + url = $url + regex = $regex + json = $json + jsonpath = $jsonpath + xpath = $xpath + reverse = $reverse + replace = $replace + } + + get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object { + (ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object { + $wc.Headers[$_.Key] = $_.Value + } } $wc.Headers.Add('Referer', (strip_filename $url)) @@ -254,6 +260,7 @@ while ($in_progress -gt 0) { $in_progress-- $state = $ev.SourceEventArgs.UserState + $result = $ev.SourceEventArgs.Result $app = $state.app $file = $state.file $json = $state.json @@ -279,7 +286,13 @@ while ($in_progress -gt 0) { } if ($url) { - $page = (Get-Encoding($wc)).GetString($ev.SourceEventArgs.Result) + $ms = New-Object System.IO.MemoryStream + $ms.Write($result, 0, $result.Length) + $ms.Seek(0, 0) | Out-Null + if ($result[0] -eq 0x1F -and $result[1] -eq 0x8B) { + $ms = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress) + } + $page = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd() } if ($script) { $page = Invoke-Command ([scriptblock]::Create($script -join "`r`n")) diff --git a/bin/scoop.ps1 b/bin/scoop.ps1 index 6d96c68b2f..fd2fd41fa7 100644 --- a/bin/scoop.ps1 +++ b/bin/scoop.ps1 @@ -20,8 +20,8 @@ switch ($subCommand) { } ({ $subCommand -in @('-v', '--version') }) { Write-Host 'Current Scoop version:' - if ((Test-CommandAvailable git) -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') { - git -C "$PSScriptRoot\.." --no-pager log --oneline HEAD -n 1 + if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') { + Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline') } else { $version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md" Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline @@ -31,9 +31,9 @@ switch ($subCommand) { Get-LocalBucket | ForEach-Object { $bucketLoc = Find-BucketDirectory $_ -Root - if ((Test-Path "$bucketLoc\.git") -and (Test-CommandAvailable git)) { + if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) { Write-Host "'$_' bucket:" - git -C "$bucketLoc" --no-pager log --oneline HEAD -n 1 + Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline') Write-Host '' } } diff --git a/bin/uninstall.ps1 b/bin/uninstall.ps1 index 3baf30ba42..98b5c8d513 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,9 @@ 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 +if (get_config USE_ISOLATED_PATH) { + Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global +} success 'Scoop has been uninstalled.' diff --git a/buckets.json b/buckets.json index b192cac40f..6d5b13712c 100644 --- a/buckets.json +++ b/buckets.json @@ -2,7 +2,7 @@ "main": "https://github.com/ScoopInstaller/Main", "extras": "https://github.com/ScoopInstaller/Extras", "versions": "https://github.com/ScoopInstaller/Versions", - "nirsoft": "https://github.com/kodybrown/scoop-nirsoft", + "nirsoft": "https://github.com/ScoopInstaller/Nirsoft", "sysinternals": "https://github.com/niheaven/scoop-sysinternals", "php": "https://github.com/ScoopInstaller/PHP", "nerd-fonts": "https://github.com/matthewjberger/scoop-nerd-fonts", diff --git a/lib/autoupdate.ps1 b/lib/autoupdate.ps1 index 6edc8791ff..bd24e04015 100644 --- a/lib/autoupdate.ps1 +++ b/lib/autoupdate.ps1 @@ -8,9 +8,9 @@ function find_hash_in_rdf([String] $url, [String] $basename) { $wc.Headers.Add('User-Agent', (Get-UserAgent)) $data = $wc.DownloadData($url) [xml]$xml = (Get-Encoding($wc)).GetString($data) - } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + } catch [System.Net.WebException] { + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return $null } @@ -24,12 +24,12 @@ function find_hash_in_textfile([String] $url, [Hashtable] $substitutions, [Strin $hashfile = $null $templates = @{ - '$md5' = '([a-fA-F0-9]{32})'; - '$sha1' = '([a-fA-F0-9]{40})'; - '$sha256' = '([a-fA-F0-9]{64})'; - '$sha512' = '([a-fA-F0-9]{128})'; - '$checksum' = '([a-fA-F0-9]{32,128})'; - '$base64' = '([a-zA-Z0-9+\/=]{24,88})'; + '$md5' = '([a-fA-F0-9]{32})' + '$sha1' = '([a-fA-F0-9]{40})' + '$sha256' = '([a-fA-F0-9]{64})' + '$sha512' = '([a-fA-F0-9]{128})' + '$checksum' = '([a-fA-F0-9]{32,128})' + '$base64' = '([a-zA-Z0-9+\/=]{24,88})' } try { @@ -37,10 +37,16 @@ function find_hash_in_textfile([String] $url, [Hashtable] $substitutions, [Strin $wc.Headers.Add('Referer', (strip_filename $url)) $wc.Headers.Add('User-Agent', (Get-UserAgent)) $data = $wc.DownloadData($url) - $hashfile = (Get-Encoding($wc)).GetString($data) + $ms = New-Object System.IO.MemoryStream + $ms.Write($data, 0, $data.Length) + $ms.Seek(0, 0) | Out-Null + if ($data[0] -eq 0x1F -and $data[1] -eq 0x8B) { + $ms = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress) + } + $hashfile = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd() } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return } @@ -50,15 +56,15 @@ function find_hash_in_textfile([String] $url, [Hashtable] $substitutions, [Strin $regex = substitute $regex $templates $false $regex = substitute $regex $substitutions $true - debug $regex if ($hashfile -match $regex) { - $hash = $matches[1] -replace '\s','' + debug $regex + $hash = $matches[1] -replace '\s', '' } # convert base64 encoded hash values if ($hash -match '^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$') { $base64 = $matches[0] - if(!($hash -match '^[a-fA-F0-9]+$') -and $hash.length -notin @(32, 40, 64, 128)) { + if (!($hash -match '^[a-fA-F0-9]+$') -and $hash.Length -notin @(32, 40, 64, 128)) { try { $hash = ([System.Convert]::FromBase64String($base64) | ForEach-Object { $_.ToString('x2') }) -join '' } catch { @@ -69,13 +75,15 @@ function find_hash_in_textfile([String] $url, [Hashtable] $substitutions, [Strin # find hash with filename in $hashfile if ($hash.Length -eq 0) { - $filenameRegex = "([a-fA-F0-9]{32,128})[\x20\t]+.*`$basename(?:[\x20\t]+\d+)?" + $filenameRegex = "([a-fA-F0-9]{32,128})[\x20\t]+.*`$basename(?:\s|$)|`$basename[\x20\t]+.*?([a-fA-F0-9]{32,128})" $filenameRegex = substitute $filenameRegex $substitutions $true if ($hashfile -match $filenameRegex) { + debug $filenameRegex $hash = $matches[1] } - $metalinkRegex = "]+>([a-fA-F0-9]{64})" + $metalinkRegex = ']+>([a-fA-F0-9]{64})' if ($hashfile -match $metalinkRegex) { + debug $metalinkRegex $hash = $matches[1] } } @@ -91,14 +99,21 @@ function find_hash_in_json([String] $url, [Hashtable] $substitutions, [String] $ $wc.Headers.Add('Referer', (strip_filename $url)) $wc.Headers.Add('User-Agent', (Get-UserAgent)) $data = $wc.DownloadData($url) - $json = (Get-Encoding($wc)).GetString($data) - } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + $ms = New-Object System.IO.MemoryStream + $ms.Write($data, 0, $data.Length) + $ms.Seek(0, 0) | Out-Null + if ($data[0] -eq 0x1F -and $data[1] -eq 0x8B) { + $ms = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress) + } + $json = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd() + } catch [System.Net.WebException] { + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return } + debug $jsonpath $hash = json_path $json $jsonpath $substitutions - if(!$hash) { + if (!$hash) { $hash = json_path_legacy $json $jsonpath $substitutions } return format_hash $hash @@ -112,10 +127,16 @@ function find_hash_in_xml([String] $url, [Hashtable] $substitutions, [String] $x $wc.Headers.Add('Referer', (strip_filename $url)) $wc.Headers.Add('User-Agent', (Get-UserAgent)) $data = $wc.DownloadData($url) - $xml = [xml]((Get-Encoding($wc)).GetString($data)) + $ms = New-Object System.IO.MemoryStream + $ms.Write($data, 0, $data.Length) + $ms.Seek(0, 0) | Out-Null + if ($data[0] -eq 0x1F -and $data[1] -eq 0x8B) { + $ms = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress) + } + $xml = [xml]((New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd()) } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return } @@ -125,13 +146,15 @@ function find_hash_in_xml([String] $url, [Hashtable] $substitutions, [String] $x } # Find all `significant namespace declarations` from the XML file - $nsList = $xml.SelectNodes("//namespace::*[not(. = ../../namespace::*)]") + $nsList = $xml.SelectNodes('//namespace::*[not(. = ../../namespace::*)]') # Then add them into the NamespaceManager $nsmgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) $nsList | ForEach-Object { $nsmgr.AddNamespace($_.LocalName, $_.Value) } + debug $xpath + debug $nsmgr # Getting hash from XML, using XPath $hash = $xml.SelectSingleNode($xpath, $nsmgr).'#text' return format_hash $hash @@ -148,16 +171,16 @@ function find_hash_in_headers([String] $url) { $req.Timeout = 2000 $req.Method = 'HEAD' $res = $req.GetResponse() - if(([int]$res.StatusCode -ge 300) -and ([int]$res.StatusCode -lt 400)) { - if($res.Headers['Digest'] -match 'SHA-256=([^,]+)' -or $res.Headers['Digest'] -match 'SHA=([^,]+)' -or $res.Headers['Digest'] -match 'MD5=([^,]+)') { + if (([int]$res.StatusCode -ge 300) -and ([int]$res.StatusCode -lt 400)) { + if ($res.Headers['Digest'] -match 'SHA-256=([^,]+)' -or $res.Headers['Digest'] -match 'SHA=([^,]+)' -or $res.Headers['Digest'] -match 'MD5=([^,]+)') { $hash = ([System.Convert]::FromBase64String($matches[1]) | ForEach-Object { $_.ToString('x2') }) -join '' debug $hash } } $res.Close() - } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + } catch [System.Net.WebException] { + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return } @@ -182,10 +205,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u $hashfile_url = substitute $config.url $substitutions debug $hashfile_url if ($hashfile_url) { - write-host -f DarkYellow 'Searching hash for ' -NoNewline - write-host -f Green $basename -NoNewline - write-host -f DarkYellow ' in ' -NoNewline - write-host -f Green $hashfile_url + Write-Host 'Searching hash for ' -ForegroundColor DarkYellow -NoNewline + Write-Host $basename -ForegroundColor Green -NoNewline + Write-Host ' in ' -ForegroundColor DarkYellow -NoNewline + Write-Host $hashfile_url -ForegroundColor Green } if ($hashmode.Length -eq 0 -and $config.url.Length -ne 0) { @@ -215,11 +238,11 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u $hashmode = 'xpath' } - if (!$hashfile_url -and $url -match "^(?:.*fosshub.com\/).*(?:\/|\?dwl=)(?.*)$") { + if (!$hashfile_url -and $url -match '^(?:.*fosshub.com\/).*(?:\/|\?dwl=)(?.*)$') { $hashmode = 'fosshub' } - if (!$hashfile_url -and $url -match "(?:downloads\.)?sourceforge.net\/projects?\/(?[^\/]+)\/(?:files\/)?(?.*)") { + if (!$hashfile_url -and $url -match '(?:downloads\.)?sourceforge.net\/projects?\/(?[^\/]+)\/(?:files\/)?(?.*)') { $hashmode = 'sourceforge' } @@ -243,7 +266,7 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u } } 'fosshub' { - $hash = find_hash_in_textfile $url $substitutions ($Matches.filename+'.*?"sha256":"([a-fA-F0-9]{64})"') + $hash = find_hash_in_textfile $url $substitutions ($matches.filename + '.*?"sha256":"([a-fA-F0-9]{64})"') } 'sourceforge' { # change the URL because downloads.sourceforge.net doesn't have checksums @@ -254,29 +277,29 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u if ($hash) { # got one! - write-host -f DarkYellow 'Found: ' -NoNewline - write-host -f Green $hash -NoNewline - write-host -f DarkYellow ' using ' -NoNewline - write-host -f Green "$((Get-Culture).TextInfo.ToTitleCase($hashmode)) Mode" + Write-Host 'Found: ' -ForegroundColor DarkYellow -NoNewline + Write-Host $hash -ForegroundColor Green -NoNewline + Write-Host ' using ' -ForegroundColor DarkYellow -NoNewline + Write-Host "$((Get-Culture).TextInfo.ToTitleCase($hashmode)) Mode" -ForegroundColor Green return $hash } elseif ($hashfile_url) { - write-host -f DarkYellow "Could not find hash in $hashfile_url" + Write-Host -f DarkYellow "Could not find hash in $hashfile_url" } - write-host -f DarkYellow 'Downloading ' -NoNewline - write-host -f Green $basename -NoNewline - write-host -f DarkYellow ' to compute hashes!' + Write-Host 'Downloading ' -ForegroundColor DarkYellow -NoNewline + Write-Host $basename -ForegroundColor Green -NoNewline + Write-Host ' to compute hashes!' -ForegroundColor DarkYellow try { Invoke-CachedDownload $app $version $url $null $null $true } catch [system.net.webexception] { - write-host -f darkred $_ - write-host -f darkred "URL $url is not valid" + Write-Host $_ -ForegroundColor DarkRed + Write-Host "URL $url is not valid" -ForegroundColor DarkRed return $null } - $file = fullpath (cache_path $app $version $url) + $file = cache_path $app $version $url $hash = (Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower() - write-host -f DarkYellow 'Computed hash: ' -NoNewline - write-host -f Green $hash + Write-Host 'Computed hash: ' -ForegroundColor DarkYellow -NoNewline + Write-Host $hash -ForegroundColor Green return $hash } @@ -346,7 +369,7 @@ function Update-ManifestProperty { $newValue = substitute $autoupdateProperty $Substitutions if (($autoupdateProperty.GetType().Name -eq 'Object[]') -and ($autoupdateProperty.Length -eq 1)) { # Make sure it's an array - $newValue = ,$newValue + $newValue = , $newValue } $Manifest.$currentProperty, $hasPropertyChanged = PropertyHelper -Property $Manifest.$currentProperty -Value $newValue $hasManifestChanged = $hasManifestChanged -or $hasPropertyChanged @@ -359,7 +382,7 @@ function Update-ManifestProperty { $newValue = substitute $autoupdateProperty $Substitutions if (($autoupdateProperty.GetType().Name -eq 'Object[]') -and ($autoupdateProperty.Length -eq 1)) { # Make sure it's an array - $newValue = ,$newValue + $newValue = , $newValue } $Manifest.architecture.$arch.$currentProperty, $hasPropertyChanged = PropertyHelper -Property $Manifest.architecture.$arch.$currentProperty -Value $newValue $hasManifestChanged = $hasManifestChanged -or $hasPropertyChanged @@ -388,25 +411,25 @@ function Get-VersionSubstitution { $firstPart = $Version.Split('-') | Select-Object -First 1 $lastPart = $Version.Split('-') | Select-Object -Last 1 $versionVariables = @{ - '$version' = $Version; - '$dotVersion' = ($Version -replace '[._-]', '.'); - '$underscoreVersion' = ($Version -replace '[._-]', '_'); - '$dashVersion' = ($Version -replace '[._-]', '-'); - '$cleanVersion' = ($Version -replace '[._-]', ''); - '$majorVersion' = $firstPart.Split('.') | Select-Object -First 1; - '$minorVersion' = $firstPart.Split('.') | Select-Object -Skip 1 -First 1; - '$patchVersion' = $firstPart.Split('.') | Select-Object -Skip 2 -First 1; - '$buildVersion' = $firstPart.Split('.') | Select-Object -Skip 3 -First 1; - '$preReleaseVersion' = $lastPart; - } - if($Version -match "(?\d+\.\d+(?:\.\d+)?)(?.*)") { - $versionVariables.Set_Item('$matchHead', $Matches['head']) - $versionVariables.Set_Item('$matchTail', $Matches['tail']) - } - if($CustomMatches) { + '$version' = $Version + '$dotVersion' = ($Version -replace '[._-]', '.') + '$underscoreVersion' = ($Version -replace '[._-]', '_') + '$dashVersion' = ($Version -replace '[._-]', '-') + '$cleanVersion' = ($Version -replace '[._-]', '') + '$majorVersion' = $firstPart.Split('.') | Select-Object -First 1 + '$minorVersion' = $firstPart.Split('.') | Select-Object -Skip 1 -First 1 + '$patchVersion' = $firstPart.Split('.') | Select-Object -Skip 2 -First 1 + '$buildVersion' = $firstPart.Split('.') | Select-Object -Skip 3 -First 1 + '$preReleaseVersion' = $lastPart + } + if ($Version -match '(?\d+\.\d+(?:\.\d+)?)(?.*)') { + $versionVariables.Add('$matchHead', $Matches['head']) + $versionVariables.Add('$matchTail', $Matches['tail']) + } + if ($CustomMatches) { $CustomMatches.GetEnumerator() | ForEach-Object { - if($_.Name -ne "0") { - $versionVariables.Set_Item('$match' + (Get-Culture).TextInfo.ToTitleCase($_.Name), $_.Value) + if ($_.Name -ne '0') { + $versionVariables.Add('$match' + (Get-Culture).TextInfo.ToTitleCase($_.Name), $_.Value) } } } diff --git a/lib/buckets.ps1 b/lib/buckets.ps1 index 2a03561210..63f6afc0a7 100644 --- a/lib/buckets.ps1 +++ b/lib/buckets.ps1 @@ -58,10 +58,18 @@ function Get-LocalBucket { .SYNOPSIS List all local buckets. #> - $bucketNames = (Get-ChildItem -Path $bucketsdir -Directory).Name + $bucketNames = [System.Collections.Generic.List[String]](Get-ChildItem -Path $bucketsdir -Directory).Name if ($null -eq $bucketNames) { return @() # Return a zero-length list instead of $null. } else { + $knownBuckets = known_buckets + for ($i = $knownBuckets.Count - 1; $i -ge 0 ; $i--) { + $name = $knownBuckets[$i] + if ($bucketNames.Contains($name)) { + [void]$bucketNames.Remove($name) + $bucketNames.Insert(0, $name) + } + } return $bucketNames } } @@ -99,11 +107,11 @@ function list_buckets { $bucket = [Ordered]@{ Name = $_ } $path = Find-BucketDirectory $_ -Root if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) { - $bucket.Source = git -C $path config remote.origin.url - $bucket.Updated = git -C $path log --format='%aD' -n 1 | Get-Date + $bucket.Source = Invoke-Git -Path $path -ArgumentList @('config', 'remote.origin.url') + $bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aD', '-n', '1') | Get-Date } else { $bucket.Source = friendly_path $path - $bucket.Updated = (Get-Item "$path\bucket").LastWriteTime + $bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime } $bucket.Manifests = Get-ChildItem "$path\bucket" -Force -Recurse -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count @@ -113,7 +121,7 @@ function list_buckets { } function add_bucket($name, $repo) { - if (!(Test-CommandAvailable git)) { + if (!(Test-GitAvailable)) { error "Git is required for buckets. Run 'scoop install git' and try again." return 1 } @@ -130,7 +138,7 @@ function add_bucket($name, $repo) { } foreach ($bucket in Get-LocalBucket) { if (Test-Path -Path "$bucketsdir\$bucket\.git") { - $remote = git -C "$bucketsdir\$bucket" config --get remote.origin.url + $remote = Invoke-Git -Path "$bucketsdir\$bucket" -ArgumentList @('config', '--get', 'remote.origin.url') if ((Convert-RepositoryUri -Uri $remote) -eq $uni_repo) { warn "Bucket $bucket already exists for $repo" return 2 @@ -139,14 +147,14 @@ function add_bucket($name, $repo) { } Write-Host 'Checking repo... ' -NoNewline - $out = git_cmd ls-remote $repo 2>&1 + $out = Invoke-Git -ArgumentList @('ls-remote', $repo) 2>&1 if ($LASTEXITCODE -ne 0) { error "'$repo' doesn't look like a valid git repository`n`nError given:`n$out" return 1 } ensure $bucketsdir | Out-Null $dir = ensure $dir - git_cmd clone "$repo" "`"$dir`"" -q + Invoke-Git -ArgumentList @('clone', $repo, $dir, '-q') Write-Host 'OK' success "The $name bucket was added successfully." return 0 @@ -169,7 +177,7 @@ function new_issue_msg($app, $bucket, $title, $body) { $bucket_path = "$bucketsdir\$bucket" if (Test-Path $bucket_path) { - $remote = git -C "$bucket_path" config --get remote.origin.url + $remote = Invoke-Git -Path $bucket_path -ArgumentList @('config', '--get', 'remote.origin.url') # Support ssh and http syntax # git@PROVIDER:USER/REPO.git # https://PROVIDER/USER/REPO.git diff --git a/lib/core.ps1 b/lib/core.ps1 index bc0365eb03..cf60f27e4c 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -1,3 +1,51 @@ +function Get-PESubsystem($filePath) { + try { + $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $binaryReader = [System.IO.BinaryReader]::new($fileStream) + + $fileStream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) | Out-Null + $peOffset = $binaryReader.ReadInt32() + + $fileStream.Seek($peOffset, [System.IO.SeekOrigin]::Begin) | Out-Null + $fileHeaderOffset = $fileStream.Position + + $fileStream.Seek(18, [System.IO.SeekOrigin]::Current) | Out-Null + $fileStream.Seek($fileHeaderOffset + 0x5C, [System.IO.SeekOrigin]::Begin) | Out-Null + + return $binaryReader.ReadInt16() + } catch { + return -1 + } finally { + $binaryReader.Close() + $fileStream.Close() + } +} + +function Set-PESubsystem($filePath, $targetSubsystem) { + try { + $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite) + $binaryReader = [System.IO.BinaryReader]::new($fileStream) + $binaryWriter = [System.IO.BinaryWriter]::new($fileStream) + + $fileStream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) | Out-Null + $peOffset = $binaryReader.ReadInt32() + + $fileStream.Seek($peOffset, [System.IO.SeekOrigin]::Begin) | Out-Null + $fileHeaderOffset = $fileStream.Position + + $fileStream.Seek(18, [System.IO.SeekOrigin]::Current) | Out-Null + $fileStream.Seek($fileHeaderOffset + 0x5C, [System.IO.SeekOrigin]::Begin) | Out-Null + + $binaryWriter.Write([System.Int16] $targetSubsystem) + } catch { + return $false + } finally { + $binaryReader.Close() + $fileStream.Close() + } + return $true +} + function Optimize-SecurityProtocol { # .NET Framework 4.7+ has a default security protocol called 'SystemDefault', # which allows the operating system to choose the best protocol to use. @@ -84,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 { @@ -99,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 @@ -128,13 +247,68 @@ function setup_proxy() { } } -function git_cmd { +function Invoke-Git { + [CmdletBinding()] + [OutputType([String])] + param( + [Parameter(Mandatory = $false, Position = 0)] + [Alias('PSPath', 'Path')] + [ValidateNotNullOrEmpty()] + [String] + $WorkingDirectory, + [Parameter(Mandatory = $true, Position = 1)] + [Alias('Args')] + [String[]] + $ArgumentList + ) + $proxy = get_config PROXY - $cmd = "git $($args | ForEach-Object { "$_ " })" - if ($proxy -and $proxy -ne 'none') { - $cmd = "SET HTTPS_PROXY=$proxy&&SET HTTP_PROXY=$proxy&&$cmd" + $git = Get-HelperPath -Helper Git + + if ($WorkingDirectory) { + $ArgumentList = @('-C', $WorkingDirectory) + $ArgumentList + } + + if([String]::IsNullOrEmpty($proxy) -or $proxy -eq 'none') { + return & $git @ArgumentList + } + + if($ArgumentList -Match '\b(clone|checkout|pull|fetch|ls-remote)\b') { + $j = Start-Job -ScriptBlock { + # convert proxy setting for git + $proxy = $using:proxy + if ($proxy -and $proxy.StartsWith('currentuser@')) { + $proxy = $proxy.Replace('currentuser@', ':@') + } + $env:HTTPS_PROXY = $proxy + $env:HTTP_PROXY = $proxy + & $using:git @using:ArgumentList + } + $o = $j | Receive-Job -Wait -AutoRemoveJob + return $o + } + + return & $git @ArgumentList +} + +function Invoke-GitLog { + [CmdletBinding()] + Param ( + [Parameter(Mandatory, ValueFromPipeline)] + [String]$Path, + [Parameter(Mandatory, ValueFromPipeline)] + [String]$CommitHash, + [String]$Name = '' + ) + Process { + if ($Name) { + if ($Name.Length -gt 12) { + $Name = "$($Name.Substring(0, 10)).." + } + $Name = "%Cgreen$($Name.PadRight(12, ' ').Substring(0, 12))%Creset " + } + Invoke-Git -Path $Path -ArgumentList @('--no-pager', 'log', '--color', '--no-decorate', "--grep='^(chore)'", '--invert-grep', '--abbrev=12', "--format=tformat: * %C(yellow)%h%Creset %<|(72,trunc)%s $Name%C(cyan)%cr%Creset", "$CommitHash..HEAD") } - cmd.exe /d /c $cmd } # helper functions @@ -200,7 +374,7 @@ function filesize($length) { } else { if ($null -eq $length) { $length = 0 - } + } "$($length) B" } } @@ -209,6 +383,7 @@ function filesize($length) { function basedir($global) { if($global) { return $globaldir } $scoopdir } function appsdir($global) { "$(basedir $global)\apps" } function shimdir($global) { "$(basedir $global)\shims" } +function modulesdir($global) { "$(basedir $global)\modules" } function appdir($app, $global) { "$(appsdir $global)\$app" } function versiondir($app, $version, $global) { "$(appdir $app $global)\$version" } @@ -293,12 +468,16 @@ Function Test-CommandAvailable { Return [Boolean](Get-Command $Name -ErrorAction Ignore) } +Function Test-GitAvailable { + return [Boolean](Get-HelperPath -Helper Git) +} + function Get-HelperPath { [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] - [ValidateSet('7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')] + [ValidateSet('Git', '7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')] [String] $Helper ) @@ -307,18 +486,21 @@ function Get-HelperPath { } process { switch ($Helper) { - '7zip' { - $HelperPath = Get-AppFilePath '7zip' '7z.exe' - if ([String]::IsNullOrEmpty($HelperPath)) { - $HelperPath = Get-AppFilePath '7zip-zstd' '7z.exe' + 'Git' { + $internalgit = (Get-AppFilePath 'git' 'mingw64\bin\git.exe'), (Get-AppFilePath 'git' 'mingw32\bin\git.exe') | Where-Object { $_ -ne $null } + if ($internalgit) { + $HelperPath = $internalgit + } else { + $HelperPath = (Get-Command git -ErrorAction Ignore).Source } } + '7zip' { $HelperPath = Get-AppFilePath '7zip' '7z.exe' } 'Lessmsi' { $HelperPath = Get-AppFilePath 'lessmsi' 'lessmsi.exe' } 'Innounp' { $HelperPath = Get-AppFilePath 'innounp' 'innounp.exe' } 'Dark' { - $HelperPath = Get-AppFilePath 'dark' 'dark.exe' + $HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe' if ([String]::IsNullOrEmpty($HelperPath)) { - $HelperPath = Get-AppFilePath 'wixtoolset' 'dark.exe' + $HelperPath = Get-AppFilePath 'dark' 'dark.exe' } } 'Aria2' { $HelperPath = Get-AppFilePath 'aria2' 'aria2c.exe' } @@ -339,8 +521,8 @@ function Get-CommandPath { ) begin { - $userShims = Convert-Path (shimdir $false) - $globalShims = fullpath (shimdir $true) # don't resolve: may not exist + $userShims = shimdir $false + $globalShims = shimdir $true } process { @@ -349,7 +531,10 @@ function Get-CommandPath { } catch { return $null } - $commandPath = if ($comm.Path -like "$userShims*" -or $comm.Path -like "$globalShims*") { + $commandPath = if ($comm.Path -like "$userShims\scoop-*.ps1") { + # Scoop aliases + $comm.Source + } elseif ($comm.Path -like "$userShims*" -or $comm.Path -like "$globalShims*") { Get-ShimTarget ($comm.Path -replace '\.exe$', '.shim') } elseif ($comm.CommandType -eq 'Application') { $comm.Source @@ -457,17 +642,47 @@ function ensure($dir) { } Convert-Path -Path $dir } +function Get-AbsolutePath { + <# + .SYNOPSIS + Get absolute path + .DESCRIPTION + Get absolute path, even if not existed + .PARAMETER Path + Path to manipulate + .OUTPUTS + System.String + Absolute path, may or maynot existed + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Path + ) + process { + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + } +} + function fullpath($path) { - # should be ~ rooted - $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path) + Show-DeprecatedWarning $MyInvocation 'Get-AbsolutePath' + return Get-AbsolutePath -Path $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 @@ -481,8 +696,7 @@ function Invoke-ExternalCommand { [CmdletBinding(DefaultParameterSetName = "Default")] [OutputType([Boolean])] param ( - [Parameter(Mandatory = $true, - Position = 0)] + [Parameter(Mandatory = $true, Position = 0)] [Alias("Path")] [ValidateNotNullOrEmpty()] [String] @@ -494,6 +708,9 @@ function Invoke-ExternalCommand { [Parameter(ParameterSetName = "UseShellExecute")] [Switch] $RunAs, + [Parameter(ParameterSetName = "UseShellExecute")] + [Switch] + $Quiet, [Alias("Msg")] [String] $Activity, @@ -523,29 +740,35 @@ function Invoke-ExternalCommand { if ($RunAs) { $Process.StartInfo.UseShellExecute = $true $Process.StartInfo.Verb = 'RunAs' - } else { - $Process.StartInfo.CreateNoWindow = $true - } - if ($FilePath -match '^((cmd|cscript|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$') { - $Process.StartInfo.Arguments = $ArgumentList -join ' ' - } elseif ($Process.StartInfo.ArgumentList.Add) { - # ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+) - # ref-1: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0 - # ref-2: https://docs.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.2#net-framework-vs-net-core - $ArgumentList | ForEach-Object { $Process.StartInfo.ArgumentList.Add($_) } - } else { - # escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) - $escapedArgs = $ArgumentList | ForEach-Object { - # escape N consecutive backslash(es), which are followed by a double quote, to 2N consecutive ones - $s = $_ -replace '(\\+)"', '$1$1"' - # escape N consecutive backslash(es), which are at the end of the string, to 2N consecutive ones - $s = $s -replace '(\\+)$', '$1$1' - # escape double quotes - $s = $s -replace '"', '\"' - # quote the argument - "`"$s`"" + } + if ($Quiet) { + $Process.StartInfo.UseShellExecute = $true + $Process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + } + if ($ArgumentList.Length -gt 0) { + $ArgumentList = $ArgumentList | ForEach-Object { [regex]::Split($_.Replace('"', ''), '(?<=(?/apps/scoop/current +$coreRoot = Split-Path $PSScriptRoot +$pathExpected = ($coreRoot -replace '\\','/') -like '*apps/scoop/current*' +if ($pathExpected) { + # Portable config is located in root directory: + # .\current\scoop\apps\\config.json <- a reversed path + # Imagine `/apps/scoop/current/` in a reversed format, + # and the directory tree: + # + # ``` + # : + # ├─apps + # ├─buckets + # ├─cache + # ├─persist + # ├─shims + # ├─config.json + # ``` + $configPortablePath = Get-AbsolutePath "$coreRoot\..\..\..\config.json" + if (Test-Path $configPortablePath) { + $configFile = $configPortablePath } } -# END NOTE +$scoopConfig = load_cfg $configFile # Scoop root directory -$scoopdir = $env:SCOOP, (get_config ROOT_PATH), "$([System.Environment]::GetFolderPath('UserProfile'))\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 +$scoopdir = $env:SCOOP, (get_config ROOT_PATH), "$PSScriptRoot\..\..\..\..", "$([System.Environment]::GetFolderPath('UserProfile'))\scoop" | Where-Object { $_ } | Select-Object -First 1 | Get-AbsolutePath # Scoop global apps directory -$globaldir = $env:SCOOP_GLOBAL, (get_config GLOBAL_PATH), "$([System.Environment]::GetFolderPath('CommonApplicationData'))\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 +$globaldir = $env:SCOOP_GLOBAL, (get_config GLOBAL_PATH), "$([System.Environment]::GetFolderPath('CommonApplicationData'))\scoop" | Where-Object { $_ } | Select-Object -First 1 | Get-AbsolutePath # Scoop cache directory # Note: Setting the SCOOP_CACHE environment variable to use a shared directory # is experimental and untested. There may be concurrency issues when # multiple users write and access cached files at the same time. # Use at your own risk. -$cachedir = $env:SCOOP_CACHE, (get_config CACHE_PATH), "$scoopdir\cache" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 +$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 diff --git a/lib/decompress.ps1 b/lib/decompress.ps1 index 91226706d1..d75f43fa7b 100644 --- a/lib/decompress.ps1 +++ b/lib/decompress.ps1 @@ -29,7 +29,7 @@ function Expand-7zipArchive { } $LogPath = "$(Split-Path $Path)\7zip.log" $DestinationPath = $DestinationPath.TrimEnd('\') - $ArgList = @('x', $Path, "-o$DestinationPath", '-y') + $ArgList = @('x', $Path, "-o$DestinationPath", '-xr!*.nsis', '-y') $IsTar = ((strip_ext $Path) -match '\.tar$') -or ($Path -match '\.t[abgpx]z2?$') if (!$IsTar -and $ExtractDir) { $ArgList += "-ir!$ExtractDir\*" @@ -46,12 +46,6 @@ function Expand-7zipArchive { if (!$Status) { abort "Failed to extract files from $Path.`nLog file:`n $(friendly_path $LogPath)`n$(new_issue_msg $app $bucket 'decompress error')" } - if (!$IsTar -and $ExtractDir) { - movedir "$DestinationPath\$ExtractDir" $DestinationPath | Out-Null - } - if (Test-Path $LogPath) { - Remove-Item $LogPath -Force - } if ($IsTar) { # Check for tar $Status = Invoke-ExternalCommand $7zPath @('l', $Path) -LogPath $LogPath @@ -63,8 +57,15 @@ function Expand-7zipArchive { abort "Failed to list files in $Path.`nNot a 7-Zip supported archive file." } } + if (!$IsTar -and $ExtractDir) { + movedir "$DestinationPath\$ExtractDir" $DestinationPath | Out-Null + # Remove temporary directory + Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore + } + if (Test-Path $LogPath) { + Remove-Item $LogPath -Force + } if ($Removal) { - # Remove original archive file if (($Path -replace '.*\.([^\.]*)$', '$1') -eq '001') { # Remove splited 7-zip archive parts Get-ChildItem "$($Path -replace '\.[^\.]*$', '').???" | Remove-Item -Force @@ -72,6 +73,7 @@ function Expand-7zipArchive { # Remove splitted RAR archive parts Get-ChildItem "$($Path -replace '\.part(\d+)\.rar$', '').part*.rar" | Remove-Item -Force } else { + # Remove original archive file Remove-Item $Path -Force } } @@ -112,17 +114,19 @@ function Expand-ZstdArchive { abort "Failed to extract files from $Path.`nLog file:`n $(friendly_path $LogPath)`n$(new_issue_msg $app $bucket 'decompress error')" } $IsTar = (strip_ext $Path) -match '\.tar$' + if ($IsTar) { + # Check for tar + $TarFile = Join-Path $DestinationPath (strip_ext (fname $Path)) + Expand-7zipArchive -Path $TarFile -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Removal + } if (!$IsTar -and $ExtractDir) { movedir (Join-Path $DestinationPath $ExtractDir) $DestinationPath | Out-Null + # Remove temporary directory + Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore } if (Test-Path $LogPath) { Remove-Item $LogPath -Force } - if ($IsTar) { - # Check for tar - $TarFile = Join-Path $DestinationPath (strip_ext (fname $Path)) - Expand-7zipArchive -Path $TarFile -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Removal - } } function Expand-MsiArchive { @@ -241,8 +245,14 @@ function Expand-ZipArchive { $OriDestinationPath = $DestinationPath $DestinationPath = "$DestinationPath\_tmp" } + # Disable progress bar to gain performance + $oldProgressPreference = $ProgressPreference + $global:ProgressPreference = 'SilentlyContinue' + # Compatible with Pscx v3 (https://github.com/Pscx/Pscx) ('Microsoft.PowerShell.Archive' is not needed for Pscx v4) Microsoft.PowerShell.Archive\Expand-Archive -Path $Path -DestinationPath $DestinationPath -Force + + $global:ProgressPreference = $oldProgressPreference if ($ExtractDir) { movedir "$DestinationPath\$ExtractDir" $OriDestinationPath | Out-Null Remove-Item $DestinationPath -Recurse -Force @@ -269,14 +279,32 @@ function Expand-DarkArchive { $Removal ) $LogPath = "$(Split-Path $Path)\dark.log" - $ArgList = @('-nologo', '-x', $DestinationPath, $Path) + $DarkPath = Get-HelperPath -Helper Dark + if ((Split-Path $DarkPath -Leaf) -eq 'wix.exe') { + $ArgList = @('burn', 'extract', $Path, '-out', $DestinationPath, '-outba', "$DestinationPath\UX") + } else { + $ArgList = @('-nologo', '-x', $DestinationPath, $Path) + } if ($Switches) { $ArgList += (-split $Switches) } - $Status = Invoke-ExternalCommand (Get-HelperPath -Helper Dark) $ArgList -LogPath $LogPath + $Status = Invoke-ExternalCommand $DarkPath $ArgList -LogPath $LogPath if (!$Status) { abort "Failed to extract files from $Path.`nLog file:`n $(friendly_path $LogPath)`n$(new_issue_msg $app $bucket 'decompress error')" } + if (Test-Path "$DestinationPath\WixAttachedContainer") { + Rename-Item "$DestinationPath\WixAttachedContainer" 'AttachedContainer' -ErrorAction Ignore + } else { + if (Test-Path "$DestinationPath\AttachedContainer\a0") { + $Xml = [xml](Get-Content -Raw "$DestinationPath\UX\manifest.xml" -Encoding utf8) + $Xml.BurnManifest.UX.Payload | ForEach-Object { + Rename-Item "$DestinationPath\UX\$($_.SourcePath)" $_.FilePath -ErrorAction Ignore + } + $Xml.BurnManifest.Payload | ForEach-Object { + Rename-Item "$DestinationPath\AttachedContainer\$($_.SourcePath)" $_.FilePath -ErrorAction Ignore + } + } + } if (Test-Path $LogPath) { Remove-Item $LogPath -Force } diff --git a/lib/depends.ps1 b/lib/depends.ps1 index 9c8d29042d..bd4ed19cf2 100644 --- a/lib/depends.ps1 +++ b/lib/depends.ps1 @@ -37,9 +37,9 @@ function Get-Dependency { if (!$manifest) { if (((Get-LocalBucket) -notcontains $bucket) -and $bucket) { - warn "Bucket '$bucket' not installed. Add it with 'scoop bucket add $bucket' or 'scoop bucket add $bucket '." + warn "Bucket '$bucket' not added. Add it with $(if($bucket -in (known_buckets)) { "'scoop bucket add $bucket' or " })'scoop bucket add $bucket '." } - abort "Couldn't find manifest for '$AppName'$(if(!$bucket) { '.' } else { " from '$bucket' bucket." })" + abort "Couldn't find manifest for '$AppName'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." } $deps = @(Get-InstallationHelper $manifest $Architecture) + @($manifest.depends) | Select-Object -Unique diff --git a/lib/getopt.ps1 b/lib/getopt.ps1 index 74ffae7c52..2de3ac6c0a 100644 --- a/lib/getopt.ps1 +++ b/lib/getopt.ps1 @@ -13,7 +13,7 @@ # following arguments are treated as non-option arguments, even if # they begin with a hyphen. The "--" itself will not be included in # the returned $opts. (POSIX-compatible) -function getopt($argv, $shortopts, $longopts) { +function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) { $opts = @{}; $rem = @() function err($msg) { @@ -24,10 +24,6 @@ function getopt($argv, $shortopts, $longopts) { return [Regex]::Escape($str) } - # ensure these are arrays - $argv = @($argv -split ' ') - $longopts = @($longopts) - for ($i = 0; $i -lt $argv.Length; $i++) { $arg = $argv[$i] if ($null -eq $arg) { continue } @@ -81,6 +77,5 @@ function getopt($argv, $shortopts, $longopts) { $rem += $arg } } - $opts, $rem } diff --git a/lib/install.ps1 b/lib/install.ps1 index cdbfdf1a79..12d4220015 100644 --- a/lib/install.ps1 +++ b/lib/install.ps1 @@ -8,13 +8,13 @@ function nightly_version($quiet = $false) { function install_app($app, $architecture, $global, $suggested, $use_cache = $true, $check_hash = $true) { $app, $manifest, $bucket, $url = Get-Manifest $app - if(!$manifest) { - abort "Couldn't find manifest for '$app'$(if($url) { " at the URL $url" })." + if (!$manifest) { + abort "Couldn't find manifest for '$app'$(if ($bucket) { " from '$bucket' bucket" } elseif ($url) { " at '$url'" })." } $version = $manifest.version - if(!$version) { abort "Manifest doesn't specify a version." } - if($version -match '[^\w\.\-\+_]') { + if (!$version) { abort "Manifest doesn't specify a version." } + if ($version -match '[^\w\.\-\+_]') { abort "Manifest version has unsupported character '$($matches[0])'." } @@ -38,12 +38,12 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru } else { $manifest | ConvertToPrettyJson } - $answer = Read-Host -Prompt "Continue installation? [Y/n]" + $answer = Read-Host -Prompt 'Continue installation? [Y/n]' if (($answer -eq 'n') -or ($answer -eq 'N')) { return } } - Write-Output "Installing '$app' ($version) [$architecture]$(if ($bucket) { " from $bucket bucket" })" + Write-Output "Installing '$app' ($version) [$architecture]$(if ($bucket) { " from '$bucket' bucket" } else { " from '$url'" })" $dir = ensure (versiondir $app $version $global) $original_dir = $dir # keep reference to real (not linked) directory @@ -71,7 +71,7 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru save_installed_manifest $app $bucket $dir $url save_install_info @{ 'architecture' = $architecture; 'url' = $url; 'bucket' = $bucket } $dir - if($manifest.suggest) { + if ($manifest.suggest) { $suggested[$app] = $manifest.suggest } @@ -81,13 +81,13 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru } function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) { - $cached = fullpath (cache_path $app $version $url) + $cached = cache_path $app $version $url - if(!(test-path $cached) -or !$use_cache) { + if (!(Test-Path $cached) -or !$use_cache) { ensure $cachedir | Out-Null Start-Download $url "$cached.download" $cookies - Move-Item "$cached.download" $cached -force - } else { write-host "Loading $(url_remote_filename $url) from cache"} + Move-Item "$cached.download" $cached -Force + } else { Write-Host "Loading $(url_remote_filename $url) from cache" } if (!($null -eq $to)) { if ($use_cache) { @@ -100,55 +100,58 @@ function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $us function Start-Download ($url, $to, $cookies) { $progress = [console]::isoutputredirected -eq $false -and - $host.name -ne 'Windows PowerShell ISE Host' + $host.name -ne 'Windows PowerShell ISE Host' try { $url = handle_special_urls $url Invoke-Download $url $to $cookies $progress } catch { $e = $_.exception - if($e.innerexception) { $e = $e.innerexception } + if ($e.Response.StatusCode -eq 'Unauthorized') { + warn 'Token might be misconfigured.' + } + if ($e.innerexception) { $e = $e.innerexception } throw $e } } function aria_exit_code($exitcode) { $codes = @{ - 0='All downloads were successful' - 1='An unknown error occurred' - 2='Timeout' - 3='Resource was not found' - 4='Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option' - 5='Download aborted because download speed was too slow. See --lowest-speed-limit option' - 6='Network problem occurred.' - 7='There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal' - 8='Remote server did not support resume when resume was required to complete download' - 9='There was not enough disk space available' - 10='Piece length was different from one in .aria2 control file. See --allow-piece-length-change option' - 11='Aria2 was downloading same file at that moment' - 12='Aria2 was downloading same info hash torrent at that moment' - 13='File already existed. See --allow-overwrite option' - 14='Renaming file failed. See --auto-file-renaming option' - 15='Aria2 could not open existing file' - 16='Aria2 could not create new file or truncate existing file' - 17='File I/O error occurred' - 18='Aria2 could not create directory' - 19='Name resolution failed' - 20='Aria2 could not parse Metalink document' - 21='FTP command failed' - 22='HTTP response header was bad or unexpected' - 23='Too many redirects occurred' - 24='HTTP authorization failed' - 25='Aria2 could not parse bencoded file (usually ".torrent" file)' - 26='".torrent" file was corrupted or missing information that aria2 needed' - 27='Magnet URI was bad' - 28='Bad/unrecognized option was given or unexpected option argument was given' - 29='The remote server was unable to handle the request due to a temporary overloading or maintenance' - 30='Aria2 could not parse JSON-RPC request' - 31='Reserved. Not used' - 32='Checksum validation failed' - } - if($null -eq $codes[$exitcode]) { + 0 = 'All downloads were successful' + 1 = 'An unknown error occurred' + 2 = 'Timeout' + 3 = 'Resource was not found' + 4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option' + 5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option' + 6 = 'Network problem occurred.' + 7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal' + 8 = 'Remote server did not support resume when resume was required to complete download' + 9 = 'There was not enough disk space available' + 10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option' + 11 = 'Aria2 was downloading same file at that moment' + 12 = 'Aria2 was downloading same info hash torrent at that moment' + 13 = 'File already existed. See --allow-overwrite option' + 14 = 'Renaming file failed. See --auto-file-renaming option' + 15 = 'Aria2 could not open existing file' + 16 = 'Aria2 could not create new file or truncate existing file' + 17 = 'File I/O error occurred' + 18 = 'Aria2 could not create directory' + 19 = 'Name resolution failed' + 20 = 'Aria2 could not parse Metalink document' + 21 = 'FTP command failed' + 22 = 'HTTP response header was bad or unexpected' + 23 = 'Too many redirects occurred' + 24 = 'HTTP authorization failed' + 25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)' + 26 = '".torrent" file was corrupted or missing information that aria2 needed' + 27 = 'Magnet URI was bad' + 28 = 'Bad/unrecognized option was given or unexpected option argument was given' + 29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance' + 30 = 'Aria2 could not parse JSON-RPC request' + 31 = 'Reserved. Not used' + 32 = 'Checksum validation failed' + } + if ($null -eq $codes[$exitcode]) { return 'An unknown error occurred' } return $codes[$exitcode] @@ -157,7 +160,7 @@ function aria_exit_code($exitcode) { function get_filename_from_metalink($file) { $bytes = get_magic_bytes_pretty $file '' # check if file starts with '"} + $dashes += switch ($p) { + 100 { '=' } + default { '>' } } # the remaining characters are filled with spaces - $spaces = switch($dashes.Length) { - $midwidth {[string]::Empty} + $spaces = switch ($dashes.Length) { + $midwidth { [string]::Empty } default { - [string]::Join("", ((1..($midwidth - $dashes.Length)) | ForEach-Object {" "})) + [string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' })) } } @@ -503,32 +514,32 @@ function Format-DownloadProgress ($url, $read, $total, $console) { } function Write-DownloadProgress ($read, $total, $url) { - $console = $host.UI.RawUI; - $left = $console.CursorPosition.X; - $top = $console.CursorPosition.Y; - $width = $console.BufferSize.Width; + $console = $host.UI.RawUI + $left = $console.CursorPosition.X + $top = $console.CursorPosition.Y + $width = $console.BufferSize.Width - if($read -eq 0) { + if ($read -eq 0) { $maxOutputLength = $(Format-DownloadProgress $url 100 $total $console).length if (($left + $maxOutputLength) -gt $width) { # not enough room to print progress on this line # print on new line - write-host + Write-Host $left = 0 - $top = $top + 1 - if($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y } + $top = $top + 1 + if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y } } } - write-host $(Format-DownloadProgress $url $read $total $console) -nonewline + Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline [console]::SetCursorPosition($left, $top) } function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) { # we only want to show this warning once - if(!$use_cache) { warn "Cache is being ignored." } + if (!$use_cache) { warn 'Cache is being ignored.' } - # can be multiple urls: if there are, then msi or installer should go last, + # can be multiple urls: if there are, then installer should go last, # so that $fname is set properly $urls = @(script:url $manifest $architecture) @@ -541,42 +552,42 @@ function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture # needs to be extracted, will get the next dir from the queue $extract_dirs = @(extract_dir $manifest $architecture) $extract_tos = @(extract_to $manifest $architecture) - $extracted = 0; + $extracted = 0 # download first - if(Test-Aria2Enabled) { + if (Test-Aria2Enabled) { Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash } else { - foreach($url in $urls) { + foreach ($url in $urls) { $fname = url_filename $url try { Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache } catch { - write-host -f darkred $_ + Write-Host -f darkred $_ abort "URL $url is not valid" } - if($check_hash) { + if ($check_hash) { $manifest_hash = hash_for_url $manifest $url $architecture $ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket) - if(!$ok) { + if (!$ok) { error $err $cached = cache_path $app $version $url - if(test-path $cached) { + if (Test-Path $cached) { # rm cached file - Remove-Item -force $cached + Remove-Item -Force $cached } - if($url.Contains('sourceforge.net')) { + if ($url.Contains('sourceforge.net')) { Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.' } - abort $(new_issue_msg $app $bucket "hash check failed") + abort $(new_issue_msg $app $bucket 'hash check failed') } } } } - foreach($url in $urls) { + foreach ($url in $urls) { $fname = url_filename $url $extract_dir = $extract_dirs[$extracted] @@ -586,32 +597,29 @@ function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture $extract_fn = $null if ($manifest.innosetup) { $extract_fn = 'Expand-InnoArchive' - } elseif($fname -match '\.zip$') { + } elseif ($fname -match '\.zip$') { # Use 7zip when available (more fast) if (((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z)) -or (Test-HelperInstalled -Helper 7zip)) { $extract_fn = 'Expand-7zipArchive' } else { $extract_fn = 'Expand-ZipArchive' } - } elseif($fname -match '\.msi$') { - # check manifest doesn't use deprecated install method - if(msi $manifest $architecture) { - warn "MSI install is deprecated. If you maintain this manifest, please refer to the manifest reference docs." - } else { - $extract_fn = 'Expand-MsiArchive' - } - } elseif(Test-ZstdRequirement -Uri $fname) { # Zstd first + } elseif ($fname -match '\.msi$') { + $extract_fn = 'Expand-MsiArchive' + } elseif (Test-ZstdRequirement -Uri $fname) { + # Zstd first $extract_fn = 'Expand-ZstdArchive' - } elseif(Test-7zipRequirement -Uri $fname) { # 7zip + } elseif (Test-7zipRequirement -Uri $fname) { + # 7zip $extract_fn = 'Expand-7zipArchive' } - if($extract_fn) { - Write-Host "Extracting " -NoNewline + if ($extract_fn) { + Write-Host 'Extracting ' -NoNewline Write-Host $fname -f Cyan -NoNewline - Write-Host " ... " -NoNewline + Write-Host ' ... ' -NoNewline & $extract_fn -Path "$dir\$fname" -DestinationPath "$dir\$extract_to" -ExtractDir $extract_dir -Removal - Write-Host "done." -f Green + Write-Host 'done.' -f Green $extracted++ } } @@ -620,7 +628,7 @@ function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture } function cookie_header($cookies) { - if(!$cookies) { return } + if (!$cookies) { return } $vals = $cookies.psobject.properties | ForEach-Object { "$($_.name)=$($_.value)" @@ -630,9 +638,7 @@ function cookie_header($cookies) { } function is_in_dir($dir, $check) { - $check = "$(fullpath $check)" - $dir = "$(fullpath $dir)" - $check -match "^$([regex]::escape("$dir"))(\\|`$)" + $check -match "^$([regex]::Escape("$dir"))([/\\]|$)" } function ftp_file_size($url) { @@ -643,29 +649,28 @@ function ftp_file_size($url) { # hashes function hash_for_url($manifest, $url, $arch) { - $hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }; + $hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null } - if($hashes.length -eq 0) { return $null } + if ($hashes.length -eq 0) { return $null } $urls = @(script:url $manifest $arch) $index = [array]::indexof($urls, $url) - if($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." } + if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." } @($hashes)[$index] } # returns (ok, err) function check_hash($file, $hash, $app_name) { - $file = fullpath $file - if(!$hash) { + if (!$hash) { warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())" return $true, $null } - Write-Host "Checking hash of " -NoNewline + Write-Host 'Checking hash of ' -NoNewline Write-Host $(url_remote_filename $url) -f Cyan -NoNewline - Write-Host " ... " -nonewline + Write-Host ' ... ' -NoNewline $algorithm, $expected = get_hash $hash if ($null -eq $algorithm) { return $false, "Hash type '$algorithm' isn't supported." @@ -674,152 +679,85 @@ function check_hash($file, $hash, $app_name) { $actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower() $expected = $expected.ToLower() - if($actual -ne $expected) { + if ($actual -ne $expected) { $msg = "Hash check failed!`n" $msg += "App: $app_name`n" $msg += "URL: $url`n" - if(Test-Path $file) { + if (Test-Path $file) { $msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n" } - if($expected -or $actual) { + if ($expected -or $actual) { $msg += "Expected: $expected`n" $msg += "Actual: $actual" } return $false, $msg } - Write-Host "ok." -f Green + Write-Host 'ok.' -f Green return $true, $null } # for dealing with installers function args($config, $dir, $global) { - if($config) { return $config | ForEach-Object { (format $_ @{'dir'=$dir;'global'=$global}) } } + if ($config) { return $config | ForEach-Object { (format $_ @{'dir' = $dir; 'global' = $global }) } } @() } function run_installer($fname, $manifest, $architecture, $dir, $global) { - # MSI or other installer - $msi = msi $manifest $architecture $installer = installer $manifest $architecture - if($installer.script) { - write-output "Running installer script..." + if ($installer.script) { + Write-Output 'Running installer script...' Invoke-Command ([scriptblock]::Create($installer.script -join "`r`n")) return } - - if($msi) { - install_msi $fname $dir $msi - } elseif($installer) { - install_prog $fname $dir $installer $global - } -} - -# deprecated (see also msi_installed) -function install_msi($fname, $dir, $msi) { - $msifile = "$dir\$(coalesce $msi.file "$fname")" - if(!(is_in_dir $dir $msifile)) { - abort "Error in manifest: MSI file $msifile is outside the app directory." - } - if(!($msi.code)) { abort "Error in manifest: Couldn't find MSI code."} - if(msi_installed $msi.code) { abort "The MSI package is already installed on this system." } - - $logfile = "$dir\install.log" - - $arg = @("/i `"$msifile`"", '/norestart', "/lvp `"$logfile`"", "TARGETDIR=`"$dir`"", - "INSTALLDIR=`"$dir`"") + @(args $msi.args $dir) - - if($msi.silent) { $arg += '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1' } - else { $arg += '/qb-!' } - - $continue_exit_codes = @{ 3010 = "a restart is required to complete installation" } - - $installed = Invoke-ExternalCommand 'msiexec' $arg -Activity "Running installer..." -ContinueExitCodes $continue_exit_codes - if(!$installed) { - abort "Installation aborted. You might need to run 'scoop uninstall $app' before trying again." - } - Remove-Item $logfile - Remove-Item $msifile -} - -# deprecated -# get-wmiobject win32_product is slow and checks integrity of each installed program, -# so this uses the [wmi] type accelerator instead -# http://blogs.technet.com/b/heyscriptingguy/archive/2011/12/14/use-powershell-to-find-and-uninstall-software.aspx -function msi_installed($code) { - $path = "hklm:\software\microsoft\windows\currentversion\uninstall\$code" - if(!(test-path $path)) { return $false } - $key = Get-Item $path - $name = $key.getvalue('displayname') - $version = $key.getvalue('displayversion') - $classkey = "IdentifyingNumber=`"$code`",Name=`"$name`",Version=`"$version`"" - try { $wmi = [wmi]"Win32_Product.$classkey"; $true } catch { $false } -} - -function install_prog($fname, $dir, $installer, $global) { - $prog = "$dir\$(coalesce $installer.file "$fname")" - if(!(is_in_dir $dir $prog)) { - abort "Error in manifest: Installer $prog is outside the app directory." - } - $arg = @(args $installer.args $dir $global) - - if($prog.endswith('.ps1')) { - & $prog @arg - } else { - $installed = Invoke-ExternalCommand $prog $arg -Activity "Running installer..." - if(!$installed) { - abort "Installation aborted. You might need to run 'scoop uninstall $app' before trying again." + if ($installer) { + $prog = "$dir\$(coalesce $installer.file "$fname")" + if (!(is_in_dir $dir $prog)) { + abort "Error in manifest: Installer $prog is outside the app directory." } - - # Don't remove installer if "keep" flag is set to true - if(!($installer.keep -eq "true")) { - Remove-Item $prog + $arg = @(args $installer.args $dir $global) + if ($prog.endswith('.ps1')) { + & $prog @arg + } else { + $installed = Invoke-ExternalCommand $prog $arg -Activity 'Running installer...' + if (!$installed) { + abort "Installation aborted. You might need to run 'scoop uninstall $app' before trying again." + } + # Don't remove installer if "keep" flag is set to true + if (!($installer.keep -eq 'true')) { + Remove-Item $prog + } } } } function run_uninstaller($manifest, $architecture, $dir) { - $msi = msi $manifest $architecture $uninstaller = uninstaller $manifest $architecture $version = $manifest.version - if($uninstaller.script) { - write-output "Running uninstaller script..." + if ($uninstaller.script) { + Write-Output 'Running uninstaller script...' Invoke-Command ([scriptblock]::Create($uninstaller.script -join "`r`n")) return } - if($msi -or $uninstaller) { - $exe = $null; $arg = $null; $continue_exit_codes = @{} - - if($msi) { - $code = $msi.code - $exe = "msiexec"; - $arg = @("/norestart", "/x $code") - if($msi.silent) { - $arg += '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1' - } else { - $arg += '/qb-!' - } - - $continue_exit_codes.1605 = 'not installed, skipping' - $continue_exit_codes.3010 = 'restart required' - } elseif($uninstaller) { - $exe = "$dir\$($uninstaller.file)" - $arg = args $uninstaller.args - if(!(is_in_dir $dir $exe)) { - warn "Error in manifest: Installer $exe is outside the app directory, skipping." - $exe = $null; - } elseif(!(test-path $exe)) { - warn "Uninstaller $exe is missing, skipping." - $exe = $null; - } + if ($uninstaller.file) { + $prog = "$dir\$($uninstaller.file)" + $arg = args $uninstaller.args + if (!(is_in_dir $dir $prog)) { + warn "Error in manifest: Installer $prog is outside the app directory, skipping." + $prog = $null + } elseif (!(Test-Path $prog)) { + warn "Uninstaller $prog is missing, skipping." + $prog = $null } - if($exe) { - if($exe.endswith('.ps1')) { - & $exe @arg + if ($prog) { + if ($prog.endswith('.ps1')) { + & $prog @arg } else { - $uninstalled = Invoke-ExternalCommand $exe $arg -Activity "Running uninstaller..." -ContinueExitCodes $continue_exit_codes - if(!$uninstalled) { abort "Uninstallation aborted." } + $uninstalled = Invoke-ExternalCommand $prog $arg -Activity 'Running uninstaller...' + if (!$uninstalled) { + abort 'Uninstallation aborted.' + } } } } @@ -827,7 +765,7 @@ function run_uninstaller($manifest, $architecture, $dir) { # get target, name, arguments for shim function shim_def($item) { - if($item -is [array]) { return $item } + if ($item -is [array]) { return $item } return $item, (strip_ext (fname $item)), $null } @@ -835,18 +773,18 @@ function create_shims($manifest, $dir, $global, $arch) { $shims = @(arch_specific 'bin' $manifest $arch) $shims | Where-Object { $_ -ne $null } | ForEach-Object { $target, $name, $arg = shim_def $_ - write-output "Creating shim for '$name'." + Write-Output "Creating shim for '$name'." - if(test-path "$dir\$target" -pathType leaf) { + if (Test-Path "$dir\$target" -PathType leaf) { $bin = "$dir\$target" - } elseif(test-path $target -pathType leaf) { + } 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."} + if (!$bin) { abort "Can't shim '$target': File doesn't exist." } - shim $bin $global $name (substitute $arg @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir}) + shim $bin $global $name (substitute $arg @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }) } } @@ -935,18 +873,18 @@ 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 + if ($removed) { + $removed | ForEach-Object { "Installer added '$(friendly_path $_)' to path. Removing." } + Set-EnvVar -Name 'PATH' -Value $fixed -Global:$global } - if(!$global) { - $fixed, $removed = find_dir_or_subdir (env 'path' $true) "$dir" - if($removed) { - $removed | ForEach-Object { warn "Installer added '$_' to system path. You might want to remove this manually (requires admin permission)."} + if (!$global) { + $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)." } } } } @@ -956,8 +894,8 @@ function find_dir_or_subdir($path, $dir) { $fixed = @() $removed = @() $path.split(';') | ForEach-Object { - if($_) { - if(($_ -eq $dir) -or ($_ -like "$dir\*")) { $removed += $_ } + if ($_) { + if (($_ -eq $dir) -or ($_ -like "$dir\*")) { $removed += $_ } else { $fixed += $_ } } } @@ -968,29 +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_first_in_path $path_dir $global + 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 - $env_add_path | Where-Object { $_ } | ForEach-Object { - $path_dir = Join-Path $dir $_ - - remove_from_path $path_dir $global + $dir = $dir.TrimEnd('\') + if ($env_add_path) { + $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 } } @@ -998,9 +928,9 @@ function env_set($manifest, $dir, $global, $arch) { $env_set = arch_specific 'env_set' $manifest $arch if ($env_set) { $env_set | Get-Member -Member NoteProperty | ForEach-Object { - $name = $_.name; - $val = format $env_set.$($_.name) @{ "dir" = $dir } - env $name $global $val + $name = $_.name + $val = format $env_set.$($_.name) @{ 'dir' = $dir } + Set-EnvVar -Name $name -Value $val -Global:$global Set-Content env:\$name $val } } @@ -1010,7 +940,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 } } } @@ -1021,7 +951,7 @@ function Invoke-HookScript { param( [Parameter(Mandatory = $true)] [ValidateSet('pre_install', 'post_install', - 'pre_uninstall', 'post_uninstall')] + 'pre_uninstall', 'post_uninstall')] [String] $HookType, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -1039,10 +969,10 @@ function Invoke-HookScript { } function show_notes($manifest, $dir, $original_dir, $persist_dir) { - if($manifest.notes) { - write-output "Notes" - write-output "-----" - write-output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir})) + if ($manifest.notes) { + Write-Output 'Notes' + Write-Output '-----' + Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir })) } } @@ -1066,13 +996,18 @@ function ensure_none_failed($apps) { foreach ($app in $apps) { $app = ($app -split '/|\\')[-1] -replace '\.json$', '' foreach ($global in $true, $false) { + if ($global) { + $instArgs = @('--global') + } else { + $instArgs = @() + } if (failed $app $global) { if (installed $app $global) { info "Repair previous failed installation of $app." - & "$PSScriptRoot\..\libexec\scoop-reset.ps1" $app$(if ($global) { ' --global' }) + & "$PSScriptRoot\..\libexec\scoop-reset.ps1" $app @instArgs } else { warn "Purging previous failed installation of $app." - & "$PSScriptRoot\..\libexec\scoop-uninstall.ps1" $app$(if ($global) { ' --global' }) + & "$PSScriptRoot\..\libexec\scoop-uninstall.ps1" $app @instArgs } } } @@ -1082,23 +1017,23 @@ function ensure_none_failed($apps) { function show_suggestions($suggested) { $installed_apps = (installed_apps $true) + (installed_apps $false) - foreach($app in $suggested.keys) { - $features = $suggested[$app] | get-member -type noteproperty | ForEach-Object { $_.name } - foreach($feature in $features) { + foreach ($app in $suggested.keys) { + $features = $suggested[$app] | Get-Member -type noteproperty | ForEach-Object { $_.name } + foreach ($feature in $features) { $feature_suggestions = $suggested[$app].$feature $fulfilled = $false - foreach($suggestion in $feature_suggestions) { + foreach ($suggestion in $feature_suggestions) { $suggested_app, $bucket, $null = parse_app $suggestion - if($installed_apps -contains $suggested_app) { - $fulfilled = $true; - break; + if ($installed_apps -contains $suggested_app) { + $fulfilled = $true + break } } - if(!$fulfilled) { - write-host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." + if (!$fulfilled) { + Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." } } } @@ -1123,22 +1058,22 @@ function persist_def($persist) { function persist_data($manifest, $original_dir, $persist_dir) { $persist = $manifest.persist - if($persist) { + if ($persist) { $persist_dir = ensure $persist_dir if ($persist -is [String]) { - $persist = @($persist); + $persist = @($persist) } $persist | ForEach-Object { $source, $target = persist_def $_ - write-host "Persisting $source" + Write-Host "Persisting $source" - $source = $source.TrimEnd("/").TrimEnd("\\") + $source = $source.TrimEnd('/').TrimEnd('\\') - $source = fullpath "$dir\$source" - $target = fullpath "$persist_dir\$target" + $source = "$dir\$source" + $target = "$persist_dir\$target" # if we have had persist data in the store, just create link and go if (Test-Path $target) { @@ -1179,7 +1114,7 @@ function unlink_persist_data($manifest, $dir) { if ($persist) { @($persist) | ForEach-Object { $source, $null = persist_def $_ - $source = Get-Item "$dir\$source" + $source = Get-Item "$dir\$source" -ErrorAction SilentlyContinue if ($source.LinkType) { $source_path = $source.FullName # directory (junction) @@ -1199,7 +1134,7 @@ function unlink_persist_data($manifest, $dir) { # check whether write permission for Users usergroup is set to global persist dir, if not then set function persist_permission($manifest, $global) { - if($global -and $manifest.persist -and (is_admin)) { + if ($global -and $manifest.persist -and (is_admin)) { $path = persistdir $null $global $user = New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-545' $target_rule = New-Object System.Security.AccessControl.FileSystemAccessRule($user, 'Write', 'ObjectInherit', 'none', 'Allow') diff --git a/lib/manifest.ps1 b/lib/manifest.ps1 index 8b8a685736..f66755c6f4 100644 --- a/lib/manifest.ps1 +++ b/lib/manifest.ps1 @@ -7,7 +7,7 @@ function parse_json($path) { try { Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop } catch { - warn "Error parsing JSON at $path." + warn "Error parsing JSON at '$path'." } } @@ -27,7 +27,7 @@ function url_manifest($url) { try { $str | ConvertFrom-Json -ErrorAction Stop } catch { - warn "Error parsing JSON at $url." + warn "Error parsing JSON at '$url'." } } @@ -44,24 +44,23 @@ function Get-Manifest($app) { if ($bucket) { $manifest = manifest $app $bucket } else { - foreach ($bucket in Get-LocalBucket) { - $manifest = manifest $app $bucket + foreach ($tekcub in Get-LocalBucket) { + $manifest = manifest $app $tekcub if ($manifest) { + $bucket = $tekcub break } } } if (!$manifest) { # couldn't find app in buckets: check if it's a local path - $appPath = $app - $bucket = $null - if (!$appPath.EndsWith('.json')) { - $appPath += '.json' - } - if (Test-Path $appPath) { - $url = Convert-Path $appPath + if (Test-Path $app) { + $url = Convert-Path $app $app = appname_from_url $url $manifest = url_manifest $url + } else { + if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app } + $app = appname_from_url $app } } } @@ -156,7 +155,6 @@ function generate_user_manifest($app, $bucket, $version) { function url($manifest, $arch) { arch_specific 'url' $manifest $arch } function installer($manifest, $arch) { arch_specific 'installer' $manifest $arch } function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $arch } -function msi($manifest, $arch) { arch_specific 'msi' $manifest $arch } function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch } function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch} function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch} diff --git a/lib/psmodules.ps1 b/lib/psmodules.ps1 index 2fbb6e6525..9d0cb7ce8a 100644 --- a/lib/psmodules.ps1 +++ b/lib/psmodules.ps1 @@ -1,22 +1,17 @@ -$modulesdir = "$scoopdir\modules" - function install_psmodule($manifest, $dir, $global) { $psmodule = $manifest.psmodule if (!$psmodule) { return } - if ($global) { - abort 'Installing PowerShell modules globally is not implemented!' - } + $targetdir = ensure (modulesdir $global) - $modulesdir = ensure $modulesdir - ensure_in_psmodulepath $modulesdir $global + ensure_in_psmodulepath $targetdir $global $module_name = $psmodule.name if (!$module_name) { abort "Invalid manifest: The 'name' property is missing from 'psmodule'." } - $linkfrom = "$modulesdir\$module_name" + $linkfrom = "$targetdir\$module_name" Write-Host "Installing PowerShell module '$module_name'" Write-Host "Linking $(friendly_path $linkfrom) => $(friendly_path $dir)" @@ -36,7 +31,9 @@ function uninstall_psmodule($manifest, $dir, $global) { $module_name = $psmodule.name Write-Host "Uninstalling PowerShell module '$module_name'." - $linkfrom = "$modulesdir\$module_name" + $targetdir = modulesdir $global + + $linkfrom = "$targetdir\$module_name" if (Test-Path $linkfrom) { Write-Host "Removing $(friendly_path $linkfrom)" $linkfrom = Convert-Path $linkfrom @@ -45,15 +42,13 @@ 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" } - $dir = fullpath $dir 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/shortcuts.ps1 b/lib/shortcuts.ps1 index e8ddec26bb..edb6357480 100644 --- a/lib/shortcuts.ps1 +++ b/lib/shortcuts.ps1 @@ -5,16 +5,16 @@ function create_startmenu_shortcuts($manifest, $dir, $global, $arch) { $target = [System.IO.Path]::Combine($dir, $_.item(0)) $target = New-Object System.IO.FileInfo($target) $name = $_.item(1) - $arguments = "" + $arguments = '' $icon = $null - if($_.length -ge 3) { + if ($_.length -ge 3) { $arguments = $_.item(2) } - if($_.length -ge 4) { + if ($_.length -ge 4) { $icon = [System.IO.Path]::Combine($dir, $_.item(3)) $icon = New-Object System.IO.FileInfo($icon) } - $arguments = (substitute $arguments @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir}) + $arguments = (substitute $arguments @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }) startmenu_shortcut $target $name $arguments $icon $global } } @@ -29,11 +29,11 @@ function shortcut_folder($global) { } function startmenu_shortcut([System.IO.FileInfo] $target, $shortcutName, $arguments, [System.IO.FileInfo]$icon, $global) { - if(!$target.Exists) { + if (!$target.Exists) { Write-Host -f DarkRed "Creating shortcut for $shortcutName ($(fname $target)) failed: Couldn't find $target" return } - if($icon -and !$icon.Exists) { + if ($icon -and !$icon.Exists) { Write-Host -f DarkRed "Creating shortcut for $shortcutName ($(fname $target)) failed: Couldn't find icon $icon" return } @@ -51,11 +51,11 @@ function startmenu_shortcut([System.IO.FileInfo] $target, $shortcutName, $argume if ($arguments) { $wsShell.Arguments = $arguments } - if($icon -and $icon.Exists) { + if ($icon -and $icon.Exists) { $wsShell.IconLocation = $icon.FullName } $wsShell.Save() - write-host "Creating shortcut for $shortcutName ($(fname $target))" + Write-Host "Creating shortcut for $shortcutName ($(fname $target))" } # Removes the Startmenu shortcut if it exists @@ -63,10 +63,10 @@ function rm_startmenu_shortcuts($manifest, $global, $arch) { $shortcuts = @(arch_specific 'shortcuts' $manifest $arch) $shortcuts | Where-Object { $_ -ne $null } | ForEach-Object { $name = $_.item(1) - $shortcut = "$(shortcut_folder $global)\$name.lnk" - write-host "Removing shortcut $(friendly_path $shortcut)" - if(Test-Path -Path $shortcut) { - Remove-Item $shortcut + $shortcut = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$(shortcut_folder $global)\$name.lnk") + Write-Host "Removing shortcut $(friendly_path $shortcut)" + if (Test-Path -Path $shortcut) { + Remove-Item $shortcut } } } diff --git a/lib/system.ps1 b/lib/system.ps1 new file mode 100644 index 0000000000..affe2c5450 --- /dev/null +++ b/lib/system.ps1 @@ -0,0 +1,176 @@ +# 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 '') { + if ($envRegisterKey.GetValue($Name)) { + $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 Split-PathLikeEnvVar { + param( + [string[]]$Pattern, + [string]$Path + ) + + if ($null -eq $Path -and $Path -eq '') { + return $null, $null + } else { + $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]$TargetEnvVar = 'PATH', + [switch]$Global, + [switch]$Force, + [switch]$Quiet + ) + + # future sessions + $inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global) + if (!$inPath -or $Force) { + 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 = Split-PathLikeEnvVar $Path $env:PATH + if (!$inPath -or $Force) { + $env:PATH = (@($Path) + $strippedPath) -join ';' + } +} + +function Remove-Path { + param( + [string[]]$Path, + [string]$TargetEnvVar = 'PATH', + [switch]$Global, + [switch]$Quiet, + [switch]$PassThru + ) + + # future sessions + $inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global) + if ($inPath) { + 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 + $inSessionPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH + if ($inSessionPath) { + $env:PATH = $strippedPath + } + if ($PassThru) { + return $inPath + } +} + +## 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 'Split-PathLikeEnvVar' + Split-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-alias.ps1 b/libexec/scoop-alias.ps1 index d60884c351..48a956fd64 100644 --- a/libexec/scoop-alias.ps1 +++ b/libexec/scoop-alias.ps1 @@ -44,13 +44,16 @@ function add_alias($name, $command) { # get current aliases from config $aliases = init_alias_config if ($aliases.$name) { - abort "Alias $name already exists." + abort "Alias '$name' already exists." } $alias_file = "scoop-$name" # generate script $shimdir = shimdir $false + if (Test-Path "$shimdir\$alias_file.ps1") { + abort "File '$alias_file.ps1' already exists in shims directory." + } $script = @( "# Summary: $description", @@ -67,18 +70,18 @@ function add_alias($name, $command) { function rm_alias($name) { $aliases = init_alias_config if (!$name) { - abort 'Which alias should be removed?' + abort 'Alias to be removed has not been specified!' } if ($aliases.$name) { - "Removing alias $name..." + info "Removing alias '$name'..." rm_shim $aliases.$name (shimdir $false) $aliases.PSObject.Properties.Remove($name) set_config $script:config_alias $aliases | Out-Null } else { - abort "Alias $name doesn't exist." + abort "Alias '$name' doesn't exist." } } diff --git a/libexec/scoop-cat.ps1 b/libexec/scoop-cat.ps1 index 3e840c7149..5cf363162d 100644 --- a/libexec/scoop-cat.ps1 +++ b/libexec/scoop-cat.ps1 @@ -14,14 +14,14 @@ if (!$app) { error ' missing'; my_usage; exit 1 } $null, $manifest, $bucket, $url = Get-Manifest $app if ($manifest) { - $style = get_config CAT_STYLE - if ($style) { - $manifest | ConvertToPrettyJson | bat --no-paging --style $style --language json - } else { - $manifest | ConvertToPrettyJson - } + $style = get_config CAT_STYLE + if ($style) { + $manifest | ConvertToPrettyJson | bat --no-paging --style $style --language json + } else { + $manifest | ConvertToPrettyJson + } } else { - abort "Couldn't find manifest for '$app'$(if($url) { " at the URL $url" })." + abort "Couldn't find manifest for '$app'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." } exit $exitCode diff --git a/libexec/scoop-checkup.ps1 b/libexec/scoop-checkup.ps1 index a775ec3964..32b0ef4a5a 100644 --- a/libexec/scoop-checkup.ps1 +++ b/libexec/scoop-checkup.ps1 @@ -10,7 +10,7 @@ $defenderIssues = 0 $adminPrivileges = ([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) -if ($adminPrivileges) { +if ($adminPrivileges -and $env:USERNAME -ne 'WDAGUtilityAccount') { $defenderIssues += !(check_windows_defender $false) $defenderIssues += !(check_windows_defender $true) } @@ -19,18 +19,18 @@ $issues += !(check_main_bucket) $issues += !(check_long_paths) $issues += !(Get-WindowsDeveloperModeStatus) -if (!(Test-HelperInstalled -Helper 7zip)) { - error "'7-Zip' is not installed! It's required for unpacking most programs. Please Run 'scoop install 7zip' or 'scoop install 7zip-zstd'." +if (!(Test-HelperInstalled -Helper 7zip) -and !(get_config USE_EXTERNAL_7ZIP)) { + warn "'7-Zip' is not installed! It's required for unpacking most programs. Please Run 'scoop install 7zip'." $issues++ } if (!(Test-HelperInstalled -Helper Innounp)) { - error "'Inno Setup Unpacker' is not installed! It's required for unpacking InnoSetup files. Please run 'scoop install innounp'." + warn "'Inno Setup Unpacker' is not installed! It's required for unpacking InnoSetup files. Please run 'scoop install innounp'." $issues++ } if (!(Test-HelperInstalled -Helper Dark)) { - error "'dark' is not installed! It's required for unpacking installers created with the WiX Toolset. Please run 'scoop install dark' or 'scoop install wixtoolset'." + warn "'dark' is not installed! It's required for unpacking installers created with the WiX Toolset. Please run 'scoop install dark' or 'scoop install wixtoolset'." $issues++ } diff --git a/libexec/scoop-config.ps1 b/libexec/scoop-config.ps1 index 09487da7c5..ff0bff3eee 100644 --- a/libexec/scoop-config.ps1 +++ b/libexec/scoop-config.ps1 @@ -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 # ------------------- # @@ -151,30 +156,12 @@ if (!$name) { } elseif ($name -like '--help') { my_usage } elseif ($name -like 'rm') { - # NOTE Scoop config file migration. Remove this after 2023/6/30 - if ($value -notin 'SCOOP_REPO', 'SCOOP_BRANCH' -and $value -in $newConfigNames.Keys) { - warn ('Config option "{0}" is deprecated, please use "{1}" instead next time.' -f $value, $newConfigNames.$value) - $value = $newConfigNames.$value - } - # END NOTE set_config $value $null | Out-Null Write-Host "'$value' has been removed" } elseif ($null -ne $value) { - # NOTE Scoop config file migration. Remove this after 2023/6/30 - if ($name -notin 'SCOOP_REPO', 'SCOOP_BRANCH' -and $name -in $newConfigNames.Keys) { - warn ('Config option "{0}" is deprecated, please use "{1}" instead next time.' -f $name, $newConfigNames.$name) - $name = $newConfigNames.$name - } - # END NOTE set_config $name $value | Out-Null Write-Host "'$name' has been set to '$value'" } else { - # NOTE Scoop config file migration. Remove this after 2023/6/30 - if ($name -notin 'SCOOP_REPO', 'SCOOP_BRANCH' -and $name -in $newConfigNames.Keys) { - warn ('Config option "{0}" is deprecated, please use "{1}" instead next time.' -f $name, $newConfigNames.$name) - $name = $newConfigNames.$name - } - # END NOTE $value = get_config $name if($null -eq $value) { Write-Host "'$name' is not set" diff --git a/libexec/scoop-create.ps1 b/libexec/scoop-create.ps1 index 3a33d09ea0..6c40e96c5e 100644 --- a/libexec/scoop-create.ps1 +++ b/libexec/scoop-create.ps1 @@ -59,7 +59,7 @@ function choose_item($list, $query) { } if (!$url) { - scoop help create + & "$PSScriptRoot\scoop-help.ps1" create } else { create_manifest $url } diff --git a/libexec/scoop-download.ps1 b/libexec/scoop-download.ps1 index 34de7ea19f..ef43f8f41d 100644 --- a/libexec/scoop-download.ps1 +++ b/libexec/scoop-download.ps1 @@ -43,7 +43,7 @@ if (is_scoop_outdated) { if ($opt.u -or $opt.'no-update-scoop') { warn "Scoop is out of date." } else { - scoop update + & "$PSScriptRoot\scoop-update.ps1" } } @@ -57,7 +57,7 @@ foreach ($curr_app in $apps) { $app, $bucket, $version = parse_app $curr_app $app, $manifest, $bucket, $url = Get-Manifest "$bucket/$app" - info "Starting download for $app..." + info "Downloading '$app'$(if ($version) { " ($version)" }) [$architecture]$(if ($bucket) { " from $bucket bucket" })" # Generate manifest if there is different version in manifest if (($null -ne $version) -and ($manifest.version -ne $version)) { @@ -70,7 +70,7 @@ foreach ($curr_app in $apps) { } if(!$manifest) { - error "Couldn't find manifest for '$app'$(if($url) { " at the URL $url" })." + error "Couldn't find manifest for '$app'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." continue } $version = $manifest.version diff --git a/libexec/scoop-hold.ps1 b/libexec/scoop-hold.ps1 index 2d52310c79..43f11b3259 100644 --- a/libexec/scoop-hold.ps1 +++ b/libexec/scoop-hold.ps1 @@ -47,15 +47,23 @@ $apps | ForEach-Object { return } - if (get_config NO_JUNCTION){ + if (get_config NO_JUNCTION) { $version = Select-CurrentVersion -App $app -Global:$global } else { $version = 'current' } $dir = versiondir $app $version $global $json = install_info $app $version $global + if (!$json) { + error "Failed to hold '$app'." + continue + } $install = @{} $json | Get-Member -MemberType Properties | ForEach-Object { $install.Add($_.Name, $json.($_.Name)) } + if ($install.hold) { + info "'$app' is already held." + continue + } $install.hold = $true save_install_info $install $dir success "$app is now held and can not be updated anymore." diff --git a/libexec/scoop-import.ps1 b/libexec/scoop-import.ps1 index 1221e127bf..383e78578e 100644 --- a/libexec/scoop-import.ps1 +++ b/libexec/scoop-import.ps1 @@ -34,20 +34,19 @@ foreach ($item in $import.buckets) { } foreach ($item in $import.apps) { + $instArgs = @() + $holdArgs = @() $info = $item.Info -Split ', ' - $global = if ('Global install' -in $info) { - ' --global' - } else { - '' + if ('Global install' -in $info) { + $instArgs += '--global' + $holdArgs += '--global' } - $arch = if ('64bit' -in $info -and '64bit' -ne $def_arch) { - ' --arch 64bit' + if ('64bit' -in $info -and '64bit' -ne $def_arch) { + $instArgs += '--arch', '64bit' } elseif ('32bit' -in $info -and '32bit' -ne $def_arch) { - ' --arch 32bit' + $instArgs += '--arch', '32bit' } elseif ('arm64' -in $info -and 'arm64' -ne $def_arch) { - ' --arch arm64' - } else { - '' + $instArgs += '--arch', 'arm64' } $app = if ($item.Source -in $bucket_names) { @@ -58,9 +57,9 @@ foreach ($item in $import.apps) { $item.Source } - & "$PSScriptRoot\scoop-install.ps1" $app$global$arch + & "$PSScriptRoot\scoop-install.ps1" $app @instArgs if ('Held package' -in $info) { - & "$PSScriptRoot\scoop-hold.ps1" $($item.Name)$global + & "$PSScriptRoot\scoop-hold.ps1" $item.Name @holdArgs } } diff --git a/libexec/scoop-info.ps1 b/libexec/scoop-info.ps1 index f4346a6025..853de647f9 100644 --- a/libexec/scoop-info.ps1 +++ b/libexec/scoop-info.ps1 @@ -1,7 +1,7 @@ -# Usage: scoop info [--verbose] +# Usage: scoop info [options] # Summary: Display information about an app -# Options: -# -v, --verbose Show full paths and URLs +# Help: Options: +# -v, --verbose Show full paths and URLs . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' @@ -84,7 +84,7 @@ if ($manifest.depends) { if (Test-Path $manifest_file) { if (Get-Command git -ErrorAction Ignore) { - $gitinfo = (git -C (Split-Path $manifest_file) log -1 -s --format='%aD#%an' $manifest_file 2> $null) -Split '#' + $gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#' } if ($gitinfo) { $item.'Updated at' = $gitinfo[0] | Get-Date @@ -112,7 +112,7 @@ if ($status.installed) { # Collect file list from each location $appFiles = Get-ChildItem $appsdir -Filter $app - $currentFiles = Get-ChildItem $appFiles -Filter (Select-CurrentVersion $app $global) + $currentFiles = Get-ChildItem $appFiles.FullName -Filter (Select-CurrentVersion $app $global) $persistFiles = Get-ChildItem $persist_dir -ErrorAction Ignore # Will fail if app does not persist data $cacheFiles = Get-ChildItem $cachedir -Filter "$app#*" @@ -120,7 +120,7 @@ if ($status.installed) { $fileTotals = @() foreach ($fileType in ($appFiles, $currentFiles, $persistFiles, $cacheFiles)) { if ($null -ne $fileType) { - $fileSum = (Get-ChildItem $fileType -Recurse | Measure-Object -Property Length -Sum).Sum + $fileSum = (Get-ChildItem $fileType.FullName -Recurse -File | Measure-Object -Property Length -Sum).Sum $fileTotals += coalesce $fileSum 0 } else { $fileTotals += 0 @@ -160,7 +160,7 @@ if ($status.installed) { $totalPackage = 0 foreach ($url in @(url $manifest (Get-DefaultArchitecture))) { try { - if (Test-Path (fullpath (cache_path $app $manifest.version $url))) { + if (Test-Path (cache_path $app $manifest.version $url)) { $cached = " (latest version is cached)" } else { $cached = $null diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1 index 994bb5b4cf..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" @@ -56,7 +57,7 @@ if (is_scoop_outdated) { if ($opt.u -or $opt.'no-update-scoop') { warn "Scoop is out of date." } else { - scoop update + & "$PSScriptRoot\scoop-update.ps1" } } diff --git a/libexec/scoop-reset.ps1 b/libexec/scoop-reset.ps1 index e10ef9e4f9..aeef05cc83 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" @@ -69,7 +70,7 @@ $apps | ForEach-Object { #region Workaround for #2952 if (test_running_process $app $global) { - continue + return } #endregion Workaround for #2952 @@ -79,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 diff --git a/libexec/scoop-search.ps1 b/libexec/scoop-search.ps1 index adef8b1835..a9013afd02 100644 --- a/libexec/scoop-search.ps1 +++ b/libexec/scoop-search.ps1 @@ -9,7 +9,7 @@ param($query) . "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest' . "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion' -$list = @() +$list = [System.Collections.Generic.List[PSCustomObject]]::new() try { $query = New-Object Regex $query, 'IgnoreCase' @@ -32,24 +32,90 @@ function bin_match($manifest, $query) { if ((strip_ext $fname) -match $query) { $fname } elseif ($alias -match $query) { $alias } } + + if ($bins) { return $bins } + else { return $false } +} + +function bin_match_json($json, $query) { + [System.Text.Json.JsonElement]$bin = [System.Text.Json.JsonElement]::new() + if (!$json.RootElement.TryGetProperty("bin", [ref] $bin)) { return $false } + $bins = @() + if($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) { + $bins += [System.IO.Path]::GetFileName($bin) + } elseif ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { + foreach($subbin in $bin.EnumerateArray()) { + if($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) { + $bins += [System.IO.Path]::GetFileName($subbin) + } elseif ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { + if([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) { + $bins += [System.IO.Path]::GetFileName($subbin[0]) + } elseif ($subbin.GetArrayLength() -ge 2 -and $subbin[1] -match $query) { + $bins += $subbin[1] + } + } + } + } + if ($bins) { return $bins } else { return $false } } function search_bucket($bucket, $query) { - $apps = apps_in_bucket (Find-BucketDirectory $bucket) | ForEach-Object { @{ name = $_ } } + $apps = Get-ChildItem (Find-BucketDirectory $bucket) -Filter '*.json' -Recurse + + $apps | ForEach-Object { + $json = [System.Text.Json.JsonDocument]::Parse([System.IO.File]::ReadAllText($_.FullName)) + $name = $_.BaseName + + if ($name -match $query) { + $list.Add([PSCustomObject]@{ + Name = $name + Version = $json.RootElement.GetProperty("version") + Source = $bucket + Binaries = "" + }) + } else { + $bin = bin_match_json $json $query + if ($bin) { + $list.Add([PSCustomObject]@{ + Name = $name + Version = $json.RootElement.GetProperty("version") + Source = $bucket + Binaries = $bin -join ' | ' + }) + } + } + } +} - if ($query) { - $apps = $apps | Where-Object { - if ($_.name -match $query) { return $true } - $bin = bin_match (manifest $_.name $bucket) $query +# fallback function for PowerShell 5 +function search_bucket_legacy($bucket, $query) { + $apps = Get-ChildItem (Find-BucketDirectory $bucket) -Filter '*.json' -Recurse + + $apps | ForEach-Object { + $manifest = [System.IO.File]::ReadAllText($_.FullName) | ConvertFrom-Json -ErrorAction Continue + $name = $_.BaseName + + if ($name -match $query) { + $list.Add([PSCustomObject]@{ + Name = $name + Version = $manifest.Version + Source = $bucket + Binaries = "" + }) + } else { + $bin = bin_match $manifest $query if ($bin) { - $_.bin = $bin - return $true + $list.Add([PSCustomObject]@{ + Name = $name + Version = $manifest.Version + Source = $bucket + Binaries = $bin -join ' | ' + }) } } } - $apps | ForEach-Object { $_.version = (Get-LatestVersion -AppName $_.name -Bucket $bucket); $_ } } function download_json($url) { @@ -96,43 +162,35 @@ function search_remotes($query) { (add them using 'scoop bucket add ')" } + $remote_list = @() $results | ForEach-Object { - $name = $_.bucket + $bucket = $_.bucket $_.results | ForEach-Object { $item = [ordered]@{} $item.Name = $_ - $item.Source = $name - $list += [PSCustomObject]$item + $item.Source = $bucket + $remote_list += [PSCustomObject]$item } } - - $list + $remote_list } -Get-LocalBucket | ForEach-Object { - $res = search_bucket $_ $query - $local_results = $local_results -or $res - if ($res) { - $name = "$_" +$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq "System.Text.Json" } - $res | ForEach-Object { - $item = [ordered]@{} - $item.Name = $_.name - $item.Version = $_.version - $item.Source = $name - $item.Binaries = "" - if ($_.bin) { $item.Binaries = $_.bin -join ' | ' } - $list += [PSCustomObject]$item - } +Get-LocalBucket | ForEach-Object { + if ($jsonTextAvailable) { + search_bucket $_ $query + } else { + search_bucket_legacy $_ $query } } -if ($list.Length -gt 0) { +if ($list.Count -gt 0) { Write-Host "Results from local buckets..." $list } -if (!$local_results -and !(github_ratelimit_reached)) { +if ($list.Count -eq 0 -and !(github_ratelimit_reached)) { $remote_results = search_remotes $query if (!$remote_results) { warn "No matches found." diff --git a/libexec/scoop-shim.ps1 b/libexec/scoop-shim.ps1 index 41263afe6a..877b65b2e9 100644 --- a/libexec/scoop-shim.ps1 +++ b/libexec/scoop-shim.ps1 @@ -12,7 +12,7 @@ # # To list all shims or matching shims, use the 'list' subcommand: # -# scoop shim list [/...] +# scoop shim list [...] # # To show a shim's information, use the 'info' subcommand: # @@ -35,6 +35,7 @@ param($SubCommand) . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\install.ps1" # for rm_shim +. "$PSScriptRoot\..\lib\system.ps1" # 'Add-Path' (indirectly) if ($SubCommand -notin @('add', 'rm', 'list', 'info', 'alter')) { if (!$SubCommand) { @@ -82,7 +83,7 @@ function Get-ShimInfo($ShimPath) { function Get-ShimPath($ShimName, $Global) { '.shim', '.ps1' | ForEach-Object { $shimPath = Join-Path (shimdir $Global) "$ShimName$_" - if (Test-Path $shimPath) { + if (Test-Path -LiteralPath $shimPath) { return $shimPath } } @@ -144,7 +145,7 @@ switch ($SubCommand) { $other | ForEach-Object { try { $pattern = $_ - [Regex]::New($pattern) + [void][Regex]::New($pattern) } catch { Write-Host "ERROR: Invalid pattern: " -ForegroundColor Red -NoNewline Write-Host $pattern -ForegroundColor Magenta diff --git a/libexec/scoop-status.ps1 b/libexec/scoop-status.ps1 index 28596ce8e3..a62cad2dd1 100644 --- a/libexec/scoop-status.ps1 +++ b/libexec/scoop-status.ps1 @@ -8,7 +8,7 @@ . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' # check if scoop needs updating -$currentdir = fullpath $(versiondir 'scoop' 'current') +$currentdir = versiondir 'scoop' 'current' $needs_update = $false $bucket_needs_update = $false $script:network_failure = $false @@ -21,10 +21,10 @@ if (!(Get-FormatData ScoopStatus)) { function Test-UpdateStatus($repopath) { if (Test-Path "$repopath\.git") { - git_cmd -C "`"$repopath`"" fetch -q origin + Invoke-Git -Path $repopath -ArgumentList @('fetch', '-q', 'origin') $script:network_failure = 128 -eq $LASTEXITCODE - $branch = git -C $repopath branch --show-current - $commits = git -C $repopath log "HEAD..origin/$branch" --oneline + $branch = Invoke-Git -Path $repopath -ArgumentList @('branch', '--show-current') + $commits = Invoke-Git -Path $repopath -ArgumentList @('log', "HEAD..origin/$branch", '--oneline') if ($commits) { return $true } else { return $false } } else { diff --git a/libexec/scoop-unhold.ps1 b/libexec/scoop-unhold.ps1 index e678247972..4e2413a340 100644 --- a/libexec/scoop-unhold.ps1 +++ b/libexec/scoop-unhold.ps1 @@ -53,8 +53,16 @@ $apps | ForEach-Object { } $dir = versiondir $app $version $global $json = install_info $app $version $global + if (!$json) { + error "Failed to unhold '$app'" + continue + } $install = @{} $json | Get-Member -MemberType Properties | ForEach-Object { $install.Add($_.Name, $json.($_.Name)) } + if (!$install.hold) { + info "'$app' is not held." + continue + } $install.hold = $null save_install_info $install $dir success "$app is no longer held and can be updated again." 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 ad670f86c0..354fb2e839 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" @@ -56,23 +57,27 @@ if(($PSVersionTable.PSVersion.Major) -lt 5) { } $show_update_log = get_config SHOW_UPDATE_LOG $true -function update_scoop($show_update_log) { +function Sync-Scoop { + [CmdletBinding()] + Param ( + [Switch]$Log + ) # Test if Scoop Core is hold if(Test-ScoopCoreOnHold) { return } # check for git - if (!(Test-CommandAvailable git)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." } + if (!(Test-GitAvailable)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." } Write-Host "Updating Scoop..." - $currentdir = fullpath $(versiondir 'scoop' 'current') + $currentdir = versiondir 'scoop' 'current' if (!(Test-Path "$currentdir\.git")) { $newdir = "$currentdir\..\new" $olddir = "$currentdir\..\old" # get git scoop - git_cmd clone -q $configRepo --branch $configBranch --single-branch "`"$newdir`"" + Invoke-Git -ArgumentList @('clone', '-q', $configRepo, '--branch', $configBranch, '--single-branch', $newdir) # check if scoop was successful downloaded if (!(Test-Path "$newdir\bin\scoop.ps1")) { @@ -85,7 +90,7 @@ function update_scoop($show_update_log) { Rename-Item $newdir 'current' -ErrorAction Stop } catch { Write-Warning $_ - abort "Scoop update failed. Folder in use. Paste $newdir into $currentdir." + abort "Scoop update failed. Folder in use. Please rename folders $currentdir to ``old`` and $newdir to ``current``." } } } else { @@ -93,18 +98,18 @@ function update_scoop($show_update_log) { Remove-Item "$currentdir\..\old" -Recurse -Force -ErrorAction SilentlyContinue } - $previousCommit = git -C "$currentdir" rev-parse HEAD - $currentRepo = git -C "$currentdir" config remote.origin.url - $currentBranch = git -C "$currentdir" branch + $previousCommit = Invoke-Git -Path $currentdir -ArgumentList @('rev-parse', 'HEAD') + $currentRepo = Invoke-Git -Path $currentdir -ArgumentList @('config', 'remote.origin.url') + $currentBranch = Invoke-Git -Path $currentdir -ArgumentList @('branch') $isRepoChanged = !($currentRepo -match $configRepo) $isBranchChanged = !($currentBranch -match "\*\s+$configBranch") # Stash uncommitted changes - if (git -C "$currentdir" diff HEAD --name-only) { + if (Invoke-Git -Path $currentdir -ArgumentList @('diff', 'HEAD', '--name-only')) { if (get_config AUTOSTASH_ON_CONFLICT) { warn "Uncommitted changes detected. Stashing..." - git -C "$currentdir" stash push -m "WIP at $([System.DateTime]::Now.ToString('o'))" -u -q + Invoke-Git -Path $currentdir -ArgumentList @('stash', 'push', '-m', "WIP at $([System.DateTime]::Now.ToString('o'))", '-u', '-q') } else { warn "Uncommitted changes detected. Update aborted." return @@ -113,26 +118,26 @@ function update_scoop($show_update_log) { # Change remote url if the repo is changed if ($isRepoChanged) { - git -C "$currentdir" config remote.origin.url "$configRepo" + Invoke-Git -Path $currentdir -ArgumentList @('config', 'remote.origin.url', $configRepo) } # Fetch and reset local repo if the repo or the branch is changed if ($isRepoChanged -or $isBranchChanged) { # Reset git fetch refs, so that it can fetch all branches (GH-3368) - git -C "$currentdir" config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' + Invoke-Git -Path $currentdir -ArgumentList @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*') # fetch remote branch - git_cmd -C "`"$currentdir`"" fetch --force origin "refs/heads/`"$configBranch`":refs/remotes/origin/$configBranch" -q + Invoke-Git -Path $currentdir -ArgumentList @('fetch', '--force', 'origin', "refs/heads/$configBranch`:refs/remotes/origin/$configBranch", '-q') # checkout and track the branch - git_cmd -C "`"$currentdir`"" checkout -B $configBranch -t origin/$configBranch -q + Invoke-Git -Path $currentdir -ArgumentList @('checkout', '-B', $configBranch, '-t', "origin/$configBranch", '-q') # reset branch HEAD - git -C "$currentdir" reset --hard origin/$configBranch -q + Invoke-Git -Path $currentdir -ArgumentList @('reset', '--hard', "origin/$configBranch", '-q') } else { - git_cmd -C "`"$currentdir`"" pull -q + Invoke-Git -Path $currentdir -ArgumentList @('pull', '-q') } $res = $lastexitcode - if ($show_update_log) { - git -C "$currentdir" --no-pager log --no-decorate --grep='^(chore)' --invert-grep --format='tformat: * %C(yellow)%h%Creset %<|(72,trunc)%s %C(cyan)%cr%Creset' "$previousCommit..HEAD" + if ($Log) { + Invoke-GitLog -Path $currentdir -CommitHash $previousCommit } if ($res -ne 0) { @@ -140,47 +145,63 @@ function update_scoop($show_update_log) { } } - # This should have been deprecated after 2019-05-12 - # if ((Get-LocalBucket) -notcontains 'main') { - # info "The main bucket of Scoop has been separated to 'https://github.com/ScoopInstaller/Main'" - # info "Adding main bucket..." - # add_bucket 'main' - # } - shim "$currentdir\bin\scoop.ps1" $false } -function update_bucket($show_update_log) { - # check for git - if (!(Test-CommandAvailable git)) { abort "Scoop uses Git to update main bucket and others. Run 'scoop install git' and try again." } +function Sync-Bucket { + Param ( + [Switch]$Log + ) + Write-Host "Updating Buckets..." + + if (!(Test-Path (Join-Path (Find-BucketDirectory 'main' -Root) '.git'))) { + info "Converting 'main' bucket to git repo..." + $status = rm_bucket 'main' + if ($status -ne 0) { + abort "Failed to remove local 'main' bucket." + } + $status = add_bucket 'main' (known_bucket_repo 'main') + if ($status -ne 0) { + abort "Failed to add remote 'main' bucket." + } + } - foreach ($bucket in Get-LocalBucket) { - Write-Host "Updating '$bucket' bucket..." - $bucketLoc = Find-BucketDirectory $bucket -Root + $buckets = Get-LocalBucket | ForEach-Object { + $path = Find-BucketDirectory $_ -Root + return @{ + name = $_ + valid = Test-Path (Join-Path $path '.git') + path = $path + } + } - if (!(Test-Path (Join-Path $bucketLoc '.git'))) { - if ($bucket -eq 'main') { - # Make sure main bucket, which was downloaded as zip, will be properly "converted" into git - Write-Host " Converting 'main' bucket to git repo..." - $status = rm_bucket 'main' - if ($status -ne 0) { - abort "Failed to remove local 'main' bucket." - } - $status = add_bucket 'main' (known_bucket_repo 'main') - if ($status -ne 0) { - abort "Failed to add remote 'main' bucket." - } - } else { - Write-Host "'$bucket' is not a git repository. Skipped." + $buckets | Where-Object { !$_.valid } | ForEach-Object { Write-Host "'$($_.name)' is not a git repository. Skipped." } + + if ($PSVersionTable.PSVersion.Major -ge 7) { + # Parallel parameter is available since PowerShell 7 + $buckets | Where-Object { $_.valid } | ForEach-Object -ThrottleLimit 5 -Parallel { + . "$using:PSScriptRoot\..\lib\core.ps1" + + $bucketLoc = $_.path + $name = $_.name + + $previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD') + Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q') + if ($using:Log) { + Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit } - continue } - - $previousCommit = git -C "$bucketLoc" rev-parse HEAD - git_cmd -C "`"$bucketLoc`"" pull -q - if ($show_update_log) { - git -C "$bucketLoc" --no-pager log --no-decorate --grep='^(chore)' --invert-grep --format='tformat: * %C(yellow)%h%Creset %<|(72,trunc)%s %C(cyan)%cr%Creset' "$previousCommit..HEAD" + } else { + $buckets | Where-Object { $_.valid } | ForEach-Object { + $bucketLoc = $_.path + $name = $_.name + + $previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD') + Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q') + if ($Log) { + Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit + } } } } @@ -220,6 +241,13 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c Write-Host "Updating '$app' ($old_version -> $version)" + #region Workaround for #2952 + if (test_running_process $app $global) { + Write-Host 'Running process detected, skip updating.' + return + } + #endregion Workaround for #2952 + # region Workaround # Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored # Remove and replace whole region after proper fix @@ -234,7 +262,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c if ($check_hash) { $manifest_hash = hash_for_url $manifest $url $architecture - $source = fullpath (cache_path $app $version $url) + $source = cache_path $app $version $url $ok, $err = check_hash $source $manifest_hash $(show_app $app $bucket) if (!$ok) { @@ -260,24 +288,17 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture - #region Workaround for #2952 - if (test_running_process $app $global) { - return - } - #endregion Workaround for #2952 - Write-Host "Uninstalling '$app' ($old_version)" run_uninstaller $old_manifest $architecture $dir rm_shims $app $old_manifest $global $architecture - env_rm_path $old_manifest $dir $global $architecture - env_rm $old_manifest $global $architecture # If a junction was used during install, that will have been used # as the reference directory. Otherwise it will just be the version # directory. $refdir = unlink_current $dir - uninstall_psmodule $old_manifest $refdir $global + env_rm_path $old_manifest $refdir $global $architecture + env_rm $old_manifest $global $architecture if ($force -and ($old_version -eq $version)) { if (!(Test-Path "$dir/../_$version.old")) { @@ -321,8 +342,8 @@ if (-not ($apps -or $all)) { error 'scoop update: --no-cache is invalid when is not specified.' exit 1 } - update_scoop $show_update_log - update_bucket $show_update_log + Sync-Scoop -Log:$show_update_log + Sync-Bucket -Log:$show_update_log set_config LAST_UPDATE ([System.DateTime]::Now.ToString('o')) | Out-Null success 'Scoop was updated successfully!' } else { @@ -336,8 +357,8 @@ if (-not ($apps -or $all)) { $apps_param = $apps if ($updateScoop) { - update_scoop $show_update_log - update_bucket $show_update_log + Sync-Scoop -Log:$show_update_log + Sync-Bucket -Log:$show_update_log set_config LAST_UPDATE ([System.DateTime]::Now.ToString('o')) | Out-Null success 'Scoop was updated successfully!' } diff --git a/libexec/scoop-virustotal.ps1 b/libexec/scoop-virustotal.ps1 index f22bbbf1c0..e1e7b9f75c 100644 --- a/libexec/scoop-virustotal.ps1 +++ b/libexec/scoop-virustotal.ps1 @@ -36,14 +36,14 @@ $opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru') if ($err) { "scoop virustotal: $err"; exit 1 } -if (!$apps) { my_usage; exit 1 } +if (!$apps -and -$all) { my_usage; exit 1 } $architecture = Format-ArchitectureString if (is_scoop_outdated) { if ($opt.u -or $opt.'no-update-scoop') { warn 'Scoop is out of date.' } else { - scoop update + & "$PSScriptRoot\scoop-update.ps1" } } @@ -136,7 +136,7 @@ Function Get-VirusTotalResultByHash ($hash, $url, $app) { warn "$app`: $unsafe/$total, see $report_url" } Default { - warn "`e[31m$app`: $unsafe/$total, see $report_url`e[0m" + warn "$([char]0x1b)[31m$app`: $unsafe/$total, see $report_url$([char]0x1b)[0m" } } $maliciousResults = $vendorResults | diff --git a/libexec/scoop-which.ps1 b/libexec/scoop-which.ps1 index d221a5db84..677b034376 100644 --- a/libexec/scoop-which.ps1 +++ b/libexec/scoop-which.ps1 @@ -4,7 +4,7 @@ param($command) if (!$command) { - 'ERROR: missing' + error ' missing' my_usage exit 1 } @@ -12,7 +12,7 @@ if (!$command) { $path = Get-CommandPath $command if ($null -eq $path) { - Write-Host "'$command' not found / not a scoop shim." + warn "'$command' not found, not a scoop shim, or a broken shim." exit 2 } else { friendly_path $path diff --git a/schema.json b/schema.json index 239218cc23..6c24d4da20 100644 --- a/schema.json +++ b/schema.json @@ -127,10 +127,6 @@ "installer": { "$ref": "#/definitions/installer" }, - "msi": { - "$ref": "#/definitions/stringOrArrayOfStrings", - "description": "Deprecated" - }, "post_install": { "$ref": "#/definitions/stringOrArrayOfStrings" }, @@ -603,10 +599,6 @@ "license": { "$ref": "#/definitions/license" }, - "msi": { - "$ref": "#/definitions/stringOrArrayOfStrings", - "description": "Deprecated" - }, "notes": { "$ref": "#/definitions/stringOrArrayOfStrings" }, diff --git a/supporting/shimexe/.gitignore b/supporting/shimexe/.gitignore deleted file mode 100644 index 23053de093..0000000000 --- a/supporting/shimexe/.gitignore +++ /dev/null @@ -1 +0,0 @@ -packages/ diff --git a/supporting/shimexe/bin/checksum.sha256 b/supporting/shimexe/bin/checksum.sha256 deleted file mode 100644 index 7a67617054..0000000000 --- a/supporting/shimexe/bin/checksum.sha256 +++ /dev/null @@ -1 +0,0 @@ -9726c3a429009a5b22bd92cb8ab96724c670e164e7240e83f27b7c8b7bd1ca39 *shim.exe diff --git a/supporting/shimexe/bin/checksum.sha512 b/supporting/shimexe/bin/checksum.sha512 deleted file mode 100644 index 915750f159..0000000000 --- a/supporting/shimexe/bin/checksum.sha512 +++ /dev/null @@ -1 +0,0 @@ -18a737674afde4d5e7e1647d8d1e98471bb260513c57739651f92fdf1647d76c92f0cd0a9bb458daf4eae4bdab9d31404162acf6d74a041e6415752b75d722e0 *shim.exe diff --git a/supporting/shimexe/bin/shim.exe b/supporting/shimexe/bin/shim.exe deleted file mode 100644 index f6f3930334..0000000000 Binary files a/supporting/shimexe/bin/shim.exe and /dev/null differ diff --git a/supporting/shimexe/build.ps1 b/supporting/shimexe/build.ps1 deleted file mode 100644 index bf90f720ae..0000000000 --- a/supporting/shimexe/build.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -Param([Switch]$Fast) -Push-Location $PSScriptRoot -. "$PSScriptRoot\..\..\lib\core.ps1" -. "$PSScriptRoot\..\..\lib\install.ps1" - -if (!$Fast) { - Write-Host "Install dependencies ..." - & "$PSScriptRoot\install.ps1" -} - -$output = "$PSScriptRoot\bin" -Write-Output 'Compiling shim.cs ...' -& "$PSScriptRoot\packages\Microsoft.Net.Compilers.Toolset\tasks\net472\csc.exe" -deterministic -platform:anycpu -nologo -optimize -target:exe -out:"$output\shim.exe" shim.cs - -Write-Output 'Computing checksums ...' -Remove-Item "$PSScriptRoot\bin\checksum.sha256" -ErrorAction Ignore -Remove-Item "$PSScriptRoot\bin\checksum.sha512" -ErrorAction Ignore -Get-ChildItem "$PSScriptRoot\bin\*" -Include *.exe, *.dll | ForEach-Object { - "$((Get-FileHash -Path $_ -Algorithm SHA256).Hash.ToLower()) *$($_.Name)" | Out-File "$PSScriptRoot\bin\checksum.sha256" -Append -Encoding oem - "$((Get-FileHash -Path $_ -Algorithm SHA512).Hash.ToLower()) *$($_.Name)" | Out-File "$PSScriptRoot\bin\checksum.sha512" -Append -Encoding oem -} -Pop-Location diff --git a/supporting/shimexe/install.ps1 b/supporting/shimexe/install.ps1 deleted file mode 100644 index 8c5ec99222..0000000000 --- a/supporting/shimexe/install.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# https://github.com/edymtt/nugetstandalone -$destinationFolder = "$PSScriptRoot\packages" -if ((Test-Path -Path $destinationFolder)) { - Remove-Item -Path $destinationFolder -Recurse | Out-Null -} - -New-Item $destinationFolder -Type Directory | Out-Null -nuget install packages.config -o $destinationFolder -ExcludeVersion diff --git a/supporting/shimexe/packages.config b/supporting/shimexe/packages.config deleted file mode 100644 index 7d8e9deda3..0000000000 --- a/supporting/shimexe/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/supporting/shimexe/shim.cs b/supporting/shimexe/shim.cs deleted file mode 100644 index 1337341ef6..0000000000 --- a/supporting/shimexe/shim.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.ComponentModel; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Runtime.InteropServices; - -namespace Scoop { - - class Program { - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] - static extern bool CreateProcess(string lpApplicationName, - string lpCommandLine, IntPtr lpProcessAttributes, - IntPtr lpThreadAttributes, bool bInheritHandles, - uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, - [In] ref STARTUPINFO lpStartupInfo, - out PROCESS_INFORMATION lpProcessInformation); - const int ERROR_ELEVATION_REQUIRED = 740; - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct STARTUPINFO { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwYSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct PROCESS_INFORMATION { - public IntPtr hProcess; - public IntPtr hThread; - public int dwProcessId; - public int dwThreadId; - } - - [DllImport("kernel32.dll", SetLastError=true)] - static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds); - const UInt32 INFINITE = 0xFFFFFFFF; - - [DllImport("kernel32.dll", SetLastError=true)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool CloseHandle(IntPtr hObject); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); - - static int Main(string[] args) { - var exe = Assembly.GetExecutingAssembly().Location; - var dir = Path.GetDirectoryName(exe); - var name = Path.GetFileNameWithoutExtension(exe); - - var configPath = Path.Combine(dir, name + ".shim"); - if(!File.Exists(configPath)) { - Console.Error.WriteLine("Couldn't find " + Path.GetFileName(configPath) + " in " + dir); - return 1; - } - - var config = Config(configPath); - var path = Get(config, "path"); - var add_args = Get(config, "args"); - - var si = new STARTUPINFO(); - var pi = new PROCESS_INFORMATION(); - - // create command line - var cmd_args = add_args ?? ""; - var pass_args = GetArgs(Environment.CommandLine); - if(!string.IsNullOrEmpty(pass_args)) { - if(!string.IsNullOrEmpty(cmd_args)) cmd_args += " "; - cmd_args += pass_args; - } - if(!string.IsNullOrEmpty(cmd_args)) cmd_args = " " + cmd_args; - var cmd = "\"" + path + "\"" + cmd_args; - - if(!CreateProcess(null, cmd, IntPtr.Zero, IntPtr.Zero, - bInheritHandles: true, - dwCreationFlags: 0, - lpEnvironment: IntPtr.Zero, // inherit parent - lpCurrentDirectory: null, // inherit parent - lpStartupInfo: ref si, - lpProcessInformation: out pi)) { - - var error = Marshal.GetLastWin32Error(); - if(error == ERROR_ELEVATION_REQUIRED) { - // Unfortunately, ShellExecute() does not allow us to run program without - // CREATE_NEW_CONSOLE, so we can not replace CreateProcess() completely. - // The good news is we are okay with CREATE_NEW_CONSOLE when we run program with elevation. - Process process = new Process(); - process.StartInfo = new ProcessStartInfo(path, cmd_args); - process.StartInfo.UseShellExecute = true; - try { - process.Start(); - } - catch(Win32Exception exception) { - return exception.ErrorCode; - } - process.WaitForExit(); - return process.ExitCode; - } - return error; - } - - WaitForSingleObject(pi.hProcess, INFINITE); - - uint exit_code = 0; - GetExitCodeProcess(pi.hProcess, out exit_code); - - // Close process and thread handles. - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - return (int)exit_code; - } - - // now uses GetArgs instead - static string Serialize(string[] args) { - return string.Join(" ", args.Select(a => a.Contains(' ') ? '"' + a + '"' : a)); - } - - // strips the program name from the command line, returns just the arguments - static string GetArgs(string cmdLine) { - if(cmdLine.StartsWith("\"")) { - var endQuote = cmdLine.IndexOf("\" ", 1); - if(endQuote < 0) return ""; - return cmdLine.Substring(endQuote + 1); - } - var space = cmdLine.IndexOf(' '); - if(space < 0 || space == cmdLine.Length - 1) return ""; - return cmdLine.Substring(space + 1); - } - - static string Get(Dictionary dic, string key) { - string value = null; - dic.TryGetValue(key, out value); - return value; - } - - static Dictionary Config(string path) { - var config = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach(var line in File.ReadAllLines(path)) { - var m = Regex.Match(line, @"([^=]+)=(.*)"); - if(m.Success) { - config[m.Groups[1].Value.Trim()] = m.Groups[2].Value.Trim(); - } - } - return config; - } - } -} diff --git a/supporting/shimexe/shim.csproj b/supporting/shimexe/shim.csproj deleted file mode 100644 index 3324f0bb06..0000000000 --- a/supporting/shimexe/shim.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Debug - AnyCPU - {381F9D2E-2355-4F84-9206-06BB9175F97B} - Exe - Scoop.Shim - Scoop.Shim - v4.5.0 - 512 - true - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - - - - diff --git a/supporting/shimexe/update.ps1 b/supporting/shimexe/update.ps1 deleted file mode 100644 index 80a59863bf..0000000000 --- a/supporting/shimexe/update.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -# https://github.com/edymtt/nugetstandalone -$destinationFolder = "$PSScriptRoot\packages" -if (!(Test-Path -Path $destinationFolder)) { - Write-Host -f Red "Run .\install.ps1 first!" - exit 1 -} - -nuget update packages.config -r $destinationFolder -Remove-Item $destinationFolder -Force -Recurse | Out-Null -nuget install packages.config -o $destinationFolder -ExcludeVersion diff --git a/supporting/shims/kiennq/.gitignore b/supporting/shims/kiennq/.gitignore deleted file mode 100644 index 41cfea69b4..0000000000 --- a/supporting/shims/kiennq/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.zip -*.bak diff --git a/supporting/shims/kiennq/Makefile b/supporting/shims/kiennq/Makefile deleted file mode 100644 index 1610fc723c..0000000000 --- a/supporting/shims/kiennq/Makefile +++ /dev/null @@ -1,52 +0,0 @@ - -VER?=2.2.1 -ZIP=shimexe.zip -URL?=https://github.com/kiennq/scoop-better-shimexe/releases/download/$(VER)/$(ZIP) -LATEST_URL?=https://github.com/kiennq/scoop-better-shimexe/releases/latest -NEWVER=$(shell cat version.txt) - -all: verify ## make download unzip verify - -version.txt: - @curl --max-redirs 0 -s -D - -o /dev/null $(LATEST_URL) | grep -i ^location | sed -E -e "s|.*/([^/]+)$$|\1|" >version.txt - @printf "%s " "Latest version is:" - @cat version.txt - -check: version.txt ## Check the version number in version.txt and update if needed - -bump: check ## Bump version number in Makefile - @rm -f Makefile.bak - @sed -i.bak -e 's|=$(VER)|=$(NEWVER)|' Makefile - @cmp --quiet Makefile{,.bak} || echo "Makefile bumped from $(VER) to $(NEWVER)" - -$(ZIP): version.txt - curl -L -s -o $(ZIP) $(URL) - @touch $@ - -download: $(ZIP) ## Download shim from https://github.com/kiennq/scoop-better-shimexe - -shim.exe: $(ZIP) - unzip -z -j -o $(ZIP) - @touch $@ - -unzip: shim.exe ## Unzip download - -verify: shim.exe ## Verify SHA256 checksum for shim.exe - sed -e "s|bin/||" checksum.sha256 | sha256sum -c - -clean: ## Clean .zip files - rm -f *.zip - -help: ## Display help text - @printf "%-8s %s\n" Target Description - @printf "%-8s %s\n" '--------' '------------------------------------------' - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-8s %s\n", $$1, $$2}' - -.PHONY: all -.PHONY: bump -.PHONY: check -.PHONY: clean -.PHONY: download -.PHONY: help -.PHONY: unzip -.PHONY: verify diff --git a/supporting/shims/kiennq/checksum.sha256 b/supporting/shims/kiennq/checksum.sha256 index 32944ff856..51fe5eeaf7 100644 --- a/supporting/shims/kiennq/checksum.sha256 +++ b/supporting/shims/kiennq/checksum.sha256 @@ -1 +1 @@ -aa685053f4a5c0e7145f2a27514c8a56ceae25b0824062326f04037937caa558 bin/shim.exe +410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe diff --git a/supporting/shims/kiennq/checksum.sha512 b/supporting/shims/kiennq/checksum.sha512 index c7014db2a0..6c39fc8386 100644 --- a/supporting/shims/kiennq/checksum.sha512 +++ b/supporting/shims/kiennq/checksum.sha512 @@ -1 +1 @@ -67c605c8163869d8ef8153c64eb09b82645cbae8228928c0fef944d0259a7b2d3791ecf4b4b01e23566916a878ee7977bfc1a59846bccf3c63bd6a1cf4f521b5 bin/shim.exe +9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe diff --git a/supporting/shims/kiennq/shim.exe b/supporting/shims/kiennq/shim.exe index 743c9f1059..3ab79dd5e6 100644 Binary files a/supporting/shims/kiennq/shim.exe and b/supporting/shims/kiennq/shim.exe differ diff --git a/supporting/shims/kiennq/version.txt b/supporting/shims/kiennq/version.txt index c043eea776..903cd9f2a0 100644 --- a/supporting/shims/kiennq/version.txt +++ b/supporting/shims/kiennq/version.txt @@ -1 +1 @@ -2.2.1 +v3.1.1 diff --git a/supporting/shims/scoopcs/checksum.sha256 b/supporting/shims/scoopcs/checksum.sha256 new file mode 100644 index 0000000000..71456d0529 --- /dev/null +++ b/supporting/shims/scoopcs/checksum.sha256 @@ -0,0 +1 @@ +0116068768fc992fc536738396b33db3dafe6b0cf0e6f54f6d1aa8b0331f3cec *shim.exe diff --git a/supporting/shims/scoopcs/checksum.sha512 b/supporting/shims/scoopcs/checksum.sha512 new file mode 100644 index 0000000000..56bcb421f2 --- /dev/null +++ b/supporting/shims/scoopcs/checksum.sha512 @@ -0,0 +1 @@ +d734c528e9f20581ed3c7aa71a458f7dff7e2780fa0c319ccb9c813cd8dbf656bd7e550b81d2aa3ee8775bff9a4e507bc0b25f075697405adca0f47d37835848 *shim.exe diff --git a/supporting/shims/scoopcs/shim.exe b/supporting/shims/scoopcs/shim.exe new file mode 100644 index 0000000000..9df58e1de1 Binary files /dev/null and b/supporting/shims/scoopcs/shim.exe differ diff --git a/supporting/shims/scoopcs/version.txt b/supporting/shims/scoopcs/version.txt new file mode 100644 index 0000000000..9084fa2f71 --- /dev/null +++ b/supporting/shims/scoopcs/version.txt @@ -0,0 +1 @@ +1.1.0 diff --git a/supporting/validator/bin/Newtonsoft.Json.Schema.dll b/supporting/validator/bin/Newtonsoft.Json.Schema.dll index f3abd1fa42..1185cf4503 100644 Binary files a/supporting/validator/bin/Newtonsoft.Json.Schema.dll and b/supporting/validator/bin/Newtonsoft.Json.Schema.dll differ diff --git a/supporting/validator/bin/Newtonsoft.Json.dll b/supporting/validator/bin/Newtonsoft.Json.dll index 7af125a246..341d08fc8b 100644 Binary files a/supporting/validator/bin/Newtonsoft.Json.dll and b/supporting/validator/bin/Newtonsoft.Json.dll differ diff --git a/supporting/validator/bin/Scoop.Validator.dll b/supporting/validator/bin/Scoop.Validator.dll index 0b7cc50fcc..d244e70241 100644 Binary files a/supporting/validator/bin/Scoop.Validator.dll and b/supporting/validator/bin/Scoop.Validator.dll differ diff --git a/supporting/validator/bin/checksum.sha256 b/supporting/validator/bin/checksum.sha256 index e5cd51da29..bd4d44ad1b 100644 --- a/supporting/validator/bin/checksum.sha256 +++ b/supporting/validator/bin/checksum.sha256 @@ -1,4 +1,4 @@ -b624949df8b0e3a6153fdfb730a7c6f4990b6592ee0d922e1788433d276610f3 *Newtonsoft.Json.dll -9abb57d73d82a2d77008321a85aff2b62e5ac68bebb54ece8668c96cc112e36b *Newtonsoft.Json.Schema.dll -0318c8221ce4d44806f8def619bcc02886be0902aab80080e6251c50c6ca53a9 *Scoop.Validator.dll -40a70bee96d108701f8f2e81392f9b79fd003f1cb4e1653ad2429753153fd7ee *validator.exe +e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d *Newtonsoft.Json.dll +9f1a8f06c284a4ee01f704d89003ddc7061846f2008094071e9adf08267849f9 *Newtonsoft.Json.Schema.dll +d11b660612ce821ec03772b73aa3b8884a0479275c70085c7e143913a41a2d28 *Scoop.Validator.dll +87f8f8db2202a3fbef6f431d0b7e20cec9d32095c441927402041f3c4076c1b6 *validator.exe diff --git a/supporting/validator/bin/checksum.sha512 b/supporting/validator/bin/checksum.sha512 index 784d5877f5..0deec2c77b 100644 --- a/supporting/validator/bin/checksum.sha512 +++ b/supporting/validator/bin/checksum.sha512 @@ -1,4 +1,4 @@ -2fdf035661f349206f58ea1feed8805b7f9517a21f9c113e7301c69de160f184c774350a12a710046e3ff6baa37345d319b6f47fd24fbba4e042d54014bee511 *Newtonsoft.Json.dll -855ab2e30c9d523c9f321ae861c5969244185f660fa47e05cec96df8e2970d19843dbd3d89a0fca845544641915d1adf4b4a2145ef568dd99da7791e5064d70e *Newtonsoft.Json.Schema.dll -338793e6127330c0b05728291fcf18441127ffb56e1bd5c0f0588cd7436605f4b852f4bb622f655896a7eb7b1262add142b200fd5f37391b47d1401becb6b81c *Scoop.Validator.dll -d497c27b48f44f4cff270d3c8801b0cecc74108f8786a4a7c40e57541308ae33a69f5456cfc43ae1ce4214038d20da9fbeac1bcf76cc58d972863b58dab18401 *validator.exe +56eb7f070929b239642dab729537dde2c2287bdb852ad9e80b5358c74b14bc2b2dded910d0e3b6304ea27eb587e5f19db0a92e1cbae6a70fb20b4ef05057e4ac *Newtonsoft.Json.dll +551e772fe2ee72b349d5c4ed5d5f8d8957d50cfcbbde7af5d5740d9652bcad626a2c00bc0d9223db7c874962187a90f9160397f243eadee1c594585ba2b155e0 *Newtonsoft.Json.Schema.dll +0a31d192c82bbd8ce50fb75dd5fe813c98bb870d54c112c600ae2e2436063cb2bd94bb206675dfe31ce89922e9a04a3d520ed579ab7198835190b67a6321a74e *Scoop.Validator.dll +58a0c37e98cac17822c7756bf6686a5fb74e711b8d986d13bd2f689f6b3b1f485fcd908d92cbc6a162a0e5974c2c5a43de57d15f1996be0aa405e41ec2ec8393 *validator.exe diff --git a/supporting/validator/bin/validator.exe b/supporting/validator/bin/validator.exe index 3781da1f21..7671654a59 100644 Binary files a/supporting/validator/bin/validator.exe and b/supporting/validator/bin/validator.exe differ diff --git a/supporting/validator/build.ps1 b/supporting/validator/build.ps1 index c61db3313d..d5aaa89046 100644 --- a/supporting/validator/build.ps1 +++ b/supporting/validator/build.ps1 @@ -4,7 +4,7 @@ Push-Location $PSScriptRoot . "$PSScriptRoot\..\..\lib\install.ps1" if (!$Fast) { - Write-Host "Install dependencies ..." + Write-Host 'Install dependencies ...' & "$PSScriptRoot\install.ps1" } diff --git a/supporting/validator/packages.config b/supporting/validator/packages.config index 2210aacff2..333c8a4faf 100644 --- a/supporting/validator/packages.config +++ b/supporting/validator/packages.config @@ -1,6 +1,6 @@  - - - + + + diff --git a/supporting/validator/validator.csproj b/supporting/validator/validator.csproj index ec3718fd25..a2c7978fff 100644 --- a/supporting/validator/validator.csproj +++ b/supporting/validator/validator.csproj @@ -1,7 +1,10 @@ - - + + Debug AnyCPU @@ -14,12 +17,15 @@ true - - packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll True - - packages\Newtonsoft.Json.Schema.3.0.15-beta2\lib\net45\Newtonsoft.Json.Schema.dll + + + packages\Newtonsoft.Json.Schema.3.0.15\lib\net45\Newtonsoft.Json.Schema.dll True @@ -41,8 +47,12 @@ - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + This project references NuGet package(s) that are missing on this computer. + Enable NuGet Package Restore to download them. For more information, see + http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - + diff --git a/test/Import-Bucket-Tests.ps1 b/test/Import-Bucket-Tests.ps1 index 789a4b24f7..96890a0a74 100644 --- a/test/Import-Bucket-Tests.ps1 +++ b/test/Import-Bucket-Tests.ps1 @@ -16,11 +16,9 @@ Describe 'Manifest validates against the schema' { } if ($env:CI -eq $true) { Set-BuildEnvironment -Force - $changedManifests = @(Get-GitChangedFile -Path $bucketDir -Include '*.json' -Commit $env:BHCommitHash) - } - $manifestFiles = (Get-ChildItem $bucketDir -Filter '*.json' -Recurse).FullName - if ($changedManifests) { - $manifestFiles = $manifestFiles | Where-Object { $_ -in $changedManifests } + $manifestFiles = @(Get-GitChangedFile -Path $bucketDir -Include '*.json' -Commit $env:BHCommitHash) + } else { + $manifestFiles = (Get-ChildItem $bucketDir -Filter '*.json' -Recurse).FullName } } BeforeAll { diff --git a/test/Scoop-00File.Tests.ps1 b/test/Scoop-00File.Tests.ps1 index 4161c37792..4ce5c57ef0 100644 --- a/test/Scoop-00File.Tests.ps1 +++ b/test/Scoop-00File.Tests.ps1 @@ -7,8 +7,7 @@ BeforeDiscovery { '[\\/]\.git[\\/]', '\.sublime-workspace$', '\.DS_Store$', - 'supporting(\\|/)validator(\\|/)packages(\\|/)*', - 'supporting(\\|/)shimexe(\\|/)packages(\\|/)*' + 'supporting(\\|/)validator(\\|/)packages(\\|/)*' ) $repo_files = (Get-ChildItem $TestPath -File -Recurse).FullName | Where-Object { $_ -inotmatch $($project_file_exclusions -join '|') } diff --git a/test/Scoop-Alias.Tests.ps1 b/test/Scoop-Alias.Tests.ps1 index bd365718a8..e6fd33e7eb 100644 --- a/test/Scoop-Alias.Tests.ps1 +++ b/test/Scoop-Alias.Tests.ps1 @@ -8,7 +8,7 @@ BeforeAll { Describe 'Manipulate Alias' -Tag 'Scoop' { BeforeAll { Mock shimdir { "$TestDrive\shims" } - Mock set_config { } + Mock set_config {} Mock get_config { @{} } $shimdir = shimdir @@ -23,23 +23,24 @@ Describe 'Manipulate Alias' -Tag 'Scoop' { & $alias_file | Should -Be 'hello, world!' } - It 'Does not change existing alias if alias exists' { + It 'Does not change existing file if its filename same as alias name' { $alias_file = "$shimdir\scoop-rm.ps1" + Mock abort {} New-Item $alias_file -Type File -Force $alias_file | Should -Exist - add_alias 'rm' 'test' - & $alias_file | Should -Not -Be 'test' + add_alias 'rm' '"test"' + Should -Invoke -CommandName abort -Times 1 -ParameterFilter { $msg -eq "File 'scoop-rm.ps1' already exists in shims directory." } } It 'Removes an existing alias' { $alias_file = "$shimdir\scoop-rm.ps1" - add_alias 'rm' '"hello, world!"' - $alias_file | Should -Exist Mock get_config { @(@{'rm' = 'scoop-rm' }) } + Mock info {} rm_alias 'rm' $alias_file | Should -Not -Exist + Should -Invoke -CommandName info -Times 1 -ParameterFilter { $msg -eq "Removing alias 'rm'..." } } } 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-GetOpts.Tests.ps1 b/test/Scoop-GetOpts.Tests.ps1 index ef4d56a950..c55781ecbf 100644 --- a/test/Scoop-GetOpts.Tests.ps1 +++ b/test/Scoop-GetOpts.Tests.ps1 @@ -17,6 +17,12 @@ Describe 'getopt' -Tag 'Scoop' { $err | Should -Be 'Option --arb requires an argument.' } + It 'handle space in quote' { + $opt, $rem, $err = getopt '-x', 'space arg' 'x:' '' + $err | Should -BeNullOrEmpty + $opt.x | Should -Be 'space arg' + } + It 'handle unrecognized short option' { $null, $null, $err = getopt '-az' 'a' '' $err | Should -Be 'Option -z not recognized.' diff --git a/test/Scoop-Install.Tests.ps1 b/test/Scoop-Install.Tests.ps1 index bafdfbe847..966c2c0c50 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" } @@ -37,8 +38,6 @@ Describe 'is_in_dir' -Tag 'Scoop', 'Windows' { It 'should work correctly' { is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse is_in_dir 'C:\test' 'C:\test\foo\baz.zip' | Should -BeTrue - - is_in_dir 'test' "$PSScriptRoot" | Should -BeTrue is_in_dir "$PSScriptRoot\..\" "$PSScriptRoot" | Should -BeFalse } } @@ -47,27 +46,28 @@ 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' { - 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" } + 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_from_path -Times 1 -ParameterFilter { $dir -like "$testdir\foo" } - Assert-MockCalled remove_from_path -Times 1 -ParameterFilter { $dir -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 } } } diff --git a/test/Scoop-Versions.Tests.ps1 b/test/Scoop-Versions.Tests.ps1 index 536f9cc7c3..7043ee07cf 100644 --- a/test/Scoop-Versions.Tests.ps1 +++ b/test/Scoop-Versions.Tests.ps1 @@ -99,7 +99,7 @@ Describe 'versions comparison' -Tag 'Scoop' { Compare-Version 'nightly-20190801' 'nightly-20200801' | Should -Be 0 } - It 'handles nightly versions with `update_nightly`' { + It "handles nightly versions with 'update_nightly'" { function get_config { $true } Mock Get-Date { '20200801' } Compare-Version 'nightly-20200801' 'nightly' | Should -Be 0