diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3c7541f7bb..d754679687 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,39 +1,29 @@
-name: Scoop Core CI Tests
+name: CI
on:
pull_request:
workflow_dispatch:
jobs:
- test_powershell:
- name: WindowsPowerShell
+ test:
+ name: Test
runs-on: windows-latest
+ strategy:
+ matrix:
+ shell: [powershell, pwsh]
+ defaults:
+ run:
+ shell: ${{ matrix.shell }}
steps:
- name: Checkout
- uses: actions/checkout@main
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
+ # Need at least 2 commits to properly gather changed files for linting
fetch-depth: 2
- - name: Init Test Suite
- uses: potatoqualitee/psmodulecache@main
+ - name: Install and cache test dependencies
+ uses: potatoqualitee/psmodulecache@ee5e9494714abf56f6efbfa51527b2aec5c761b8 # v6.2.1
with:
- modules-to-cache: BuildHelpers
- shell: powershell
- - name: Test Scoop Core
- shell: powershell
- run: ./test/bin/test.ps1
- test_pwsh:
- name: PowerShell
- runs-on: windows-latest
- steps:
- - name: Checkout
- uses: actions/checkout@main
- with:
- fetch-depth: 2
- - name: Init Test Suite
- uses: potatoqualitee/psmodulecache@main
- with:
- modules-to-cache: BuildHelpers
- shell: pwsh
- - name: Test Scoop Core
- shell: pwsh
+ modules-to-cache: PSScriptAnalyzer, BuildHelpers, Pester
+ shell: ${{ matrix.shell }}
+ - name: Run tests
run: ./test/bin/test.ps1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0c15ec326..48dbf806bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,69 @@
+## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/v0.5.3...develop)
+
+### Features
+
+- **install:** Add output for the setting and removal of environment variables ([#6460](https://github.com/ScoopInstaller/Scoop/issues/6460))
+- **scoop-uninstall:** Allow access to `$bucket` in uninstall scripts ([#6380](https://github.com/ScoopInstaller/Scoop/issues/6380))
+- **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418))
+- **download|scoop-download:** Add GitHub issue prompt when the default downloader fails ([#6539](https://github.com/ScoopInstaller/Scoop/issues/6539))
+- **download|scoop-config:** Allow disabling automatic fallback to the default downloader when Aria2c download fails ([#6538](https://github.com/ScoopInstaller/Scoop/issues/6538))
+- **checkver:** Set GitHub mode default jsonpath and regex ([#6653](https://github.com/ScoopInstaller/Scoop/issues/6653))
+
+### Bug Fixes
+
+- **core:** Fix the grep parameter in the `Invoke-GitLog` function ([#6407](https://github.com/ScoopInstaller/Scoop/issues/6407))
+- **buckets:** Skip Git invocation if unavailable in `new_issue_msg` ([#6591](https://github.com/ScoopInstaller/Scoop/issues/6591))
+- **buckets:** Fix the filtering condition when retrieving the number of manifests ([#6509](https://github.com/ScoopInstaller/Scoop/issues/6509))
+- **scoop-download:** Fix function `nightly_version` not defined error ([#6386](https://github.com/ScoopInstaller/Scoop/issues/6386))
+- **scoop-download:** Fix incorrect download success state ([#6473](https://github.com/ScoopInstaller/Scoop/issues/6473))
+- **scoop-uninstall:** Correct `-Global` Switch ([#6454](https://github.com/ScoopInstaller/Scoop/issues/6454))
+- **scoop-update:** Force sync tags w/ remote branch while scoop update ([#6439](https://github.com/ScoopInstaller/Scoop/issues/6439))
+- **autoupdate:** Use origin URL to handle URLs with fragment in GitHub mode ([#6455](https://github.com/ScoopInstaller/Scoop/issues/6455))
+- **autoupdate:** Ensure GitHub API requests use token ([#6535](https://github.com/ScoopInstaller/Scoop/issues/6535))
+- **buckets|scoop-info:** Switch git log date format to ISO 8601 to avoid locale issues ([#6446](https://github.com/ScoopInstaller/Scoop/issues/6446))
+- **scoop-version:** Fix logic error caused by missing brackets ([#6463](https://github.com/ScoopInstaller/Scoop/issues/6463))
+- **getopt:** Teach getopt to respect the `--%` token ([#6477](https://github.com/ScoopInstaller/Scoop/issues/6477))
+- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
+- **path:** Trim ending slash when initializing paths ([#6501](https://github.com/ScoopInstaller/Scoop/issues/6501))
+- **checkver:** Allow script to run when URL fetch fails but script exists ([#6490](https://github.com/ScoopInstaller/Scoop/issues/6490), [#6556](https://github.com/ScoopInstaller/Scoop/issues/6556))
+- **install:** Don't add to `$Error` when checking for service `cexecsvc` ([#6520](https://github.com/ScoopInstaller/Scoop/issues/6520))
+- **checkver:** Fix incorrect version returned when script fails without output ([#6547](https://github.com/ScoopInstaller/Scoop/issues/6547))
+- **uninstall:** Import `url_filename` from `download.ps1` ([#6530](https://github.com/ScoopInstaller/Scoop/issues/6530))
+- **schema:** Add missing `hash.mode` value `github` ([#6533](https://github.com/ScoopInstaller/Scoop/issues/6533))
+- **core:** Skip NO_JUNCTION logic when $app is 'scoop' in `currentdir` function ([#6541](https://github.com/ScoopInstaller/Scoop/issues/6541))
+- **core:** Fix substitute handling of substring keys ([#6561](https://github.com/ScoopInstaller/Scoop/issues/6561))
+- **core:** Check `$deprecated_dir` exists before accessing it ([#6574](https://github.com/ScoopInstaller/Scoop/issues/6574))
+- **checkver:** Remove redundant always-true condition in GitHub checkver logic ([#6571](https://github.com/ScoopInstaller/Scoop/issues/6571))
+- **core:** Give `dark` higher priority when use `Extract-DarkArchive` ([#6637](https://github.com/ScoopInstaller/Scoop/issues/6637))
+- **checkver:** Harden github checkver ([#6641](https://github.com/ScoopInstaller/Scoop/issues/6641))
+- **scoop-search:** Select latest search result semantically ([#6643](https://github.com/ScoopInstaller/Scoop/issues/6643))
+
+### Code Refactoring
+
+- **output:** Replace raw prints with functions for standardized output ([#6449](https://github.com/ScoopInstaller/Scoop/issues/6449))
+- **output:** Combine the separated outputs into a single output ([#6545](https://github.com/ScoopInstaller/Scoop/issues/6545))
+- **scoop-list:** Use simpler method to check the deprecated status of the manifest to improve performance ([#6599](https://github.com/ScoopInstaller/Scoop/issues/6599))
+
+### Builds
+
+- **supporting:** Update System.Data.SQLite to 2.0.2 ([#6555](https://github.com/ScoopInstaller/Scoop/issues/6555), [#6560](https://github.com/ScoopInstaller/Scoop/issues/6560))
+
+### Continuous Integration
+
+- **workflow:** Drop Appveyor support and refactor GitHub Actions workflow ([#6626](https://github.com/ScoopInstaller/Scoop/issues/6626))
+
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11
### Features
-**autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
+- **autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
### Bug Fixes
- **scoop-download|install|update:** Fallback to default downloader when aria2 fails ([#4292](https://github.com/ScoopInstaller/Scoop/issues/4292))
-- **decompress**: `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
-- **decompress**: Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config with USE_EXTERNAL_7ZIP ([#6327](https://github.com/ScoopInstaller/Scoop/issues/6327))
-- **commands**: Handling broken aliases ([#6141](https://github.com/ScoopInstaller/Scoop/issues/6141))
+- **decompress:** `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
+- **decompress:** Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config with USE_EXTERNAL_7ZIP ([#6327](https://github.com/ScoopInstaller/Scoop/issues/6327))
+- **commands:** Handling broken aliases ([#6141](https://github.com/ScoopInstaller/Scoop/issues/6141))
- **shim:** Do not suppress `stderr`, properly check `wslpath`/`cygpath` command first ([#6114](https://github.com/ScoopInstaller/Scoop/issues/6114))
- **scoop-bucket:** Add missing import for `no_junction` envs ([#6181](https://github.com/ScoopInstaller/Scoop/issues/6181))
- **scoop-uninstall:** Fix uninstaller does not gain Global state ([#6430](https://github.com/ScoopInstaller/Scoop/issues/6430))
diff --git a/README.md b/README.md
index 641c0f6d76..52799bbe05 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,6 @@
-
-
-
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index f66a0c70d8..0000000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-version: "{build}-{branch}"
-branches:
- except:
- - gh-pages
-build: false
-deploy: false
-clone_depth: 2
-image: Visual Studio 2022
-environment:
- matrix:
- - PowerShell: 5
- - PowerShell: 7
-matrix:
- fast_finish: true
-for:
- - matrix:
- only:
- - PowerShell: 5
- cache:
- - '%USERPROFILE%\Documents\WindowsPowerShell\Modules -> appveyor.yml, test\bin\*.ps1'
- - C:\projects\helpers -> appveyor.yml, test\bin\*.ps1
- install:
- - ps: .\test\bin\init.ps1
- test_script:
- - ps: .\test\bin\test.ps1
- - matrix:
- only:
- - PowerShell: 7
- cache:
- - '%USERPROFILE%\Documents\PowerShell\Modules -> appveyor.yml, test\bin\*.ps1'
- - C:\projects\helpers -> appveyor.yml, test\bin\*.ps1
- install:
- - pwsh: .\test\bin\init.ps1
- test_script:
- - pwsh: .\test\bin\test.ps1
diff --git a/bin/checkver.ps1 b/bin/checkver.ps1
index 33a4449488..80aa7931eb 100644
--- a/bin/checkver.ps1
+++ b/bin/checkver.ps1
@@ -108,6 +108,7 @@ Get-Event | Remove-Event
Get-EventSubscriber | Unregister-Event
# start all downloads
+$in_progress = 0
$Queue | ForEach-Object {
$name, $json, $file = $_
@@ -119,47 +120,73 @@ $Queue | ForEach-Object {
} else {
$wc.Headers.Add('User-Agent', (Get-UserAgent))
}
- Register-ObjectEvent $wc downloadDataCompleted -ErrorAction Stop | Out-Null
# Not Specified
+ $url = $json.homepage
+ $regex = ''
+ $jsonpath = ''
+ $xpath = ''
+ $replace = ''
+
if ($json.checkver.url) {
$url = $json.checkver.url
- } else {
- $url = $json.homepage
}
if ($json.checkver.re) {
$regex = $json.checkver.re
} elseif ($json.checkver.regex) {
$regex = $json.checkver.regex
- } else {
- $regex = ''
}
- $jsonpath = ''
- $xpath = ''
- $replace = ''
- $useGithubAPI = $false
+ ## GitHub
+ #
+ # ```json
+ # "homepage": "",
+ # "checkver": "github"
+ # ```
+ #
+ # or
+ #
+ # ```json
+ # "checkver": {
+ # "github": ""
+ # }
+ # ```
+ if (($json.checkver -eq 'github') -or $json.checkver.github) {
+ $githubUrlPattern = '^https://((www\.)?github\.com/[\w.-]+/[\w.-]+/?|api\.github\.com/repos/[\w.-]+/[\w.-]+/.+)$'
+
+ $inputGithubUrl = $json.homepage
+ $fieldUsed = 'homepage'
+ if ($json.checkver.github) {
+ $inputGithubUrl = $json.checkver.github
+ $fieldUsed = 'checkver.github'
+ }
- # GitHub
- if ($regex) {
- $githubRegex = $regex
- } else {
- $githubRegex = '/releases/tag/(?:v|V)?([\d.]+)'
- }
- if ($json.checkver -eq 'github') {
- if (!$json.homepage.StartsWith('https://github.com/')) {
- error "$name checkver expects the homepage to be a github repository"
+ if ($inputGithubUrl -notmatch $githubUrlPattern) {
+ error "$name checkver expects $fieldUsed to be a valid GitHub repository URL"
+ return
+ }
+
+ $url = $inputGithubUrl.TrimEnd('/')
+ if ($url -notlike 'https://api.github.com*') {
+ $url = $url + '/releases/latest'
+ }
+
+ if ($GitHubToken) {
+ $url = $url -replace '//(www\.)?github\.com/', '//api.github.com/repos/'
+ $wc.Headers.Add('Authorization', "token $GitHubToken")
}
- $url = $json.homepage.TrimEnd('/') + '/releases/latest'
- $regex = $githubRegex
- $useGithubAPI = $true
- }
- if ($json.checkver.github) {
- $url = $json.checkver.github.TrimEnd('/') + '/releases/latest'
- $regex = $githubRegex
- if ($json.checkver.PSObject.Properties.Count -eq 1) { $useGithubAPI = $true }
+ if ($url -like 'https://api.github.com*') {
+ # Default jsonpath/regex in GitHub API mode
+ $jsonpath = '$.tag_name'
+ if (-not $regex) {
+ $regex = '(?:v|V)?([\d.-]+)'
+ }
+ } elseif (-not $regex) {
+ # Default regex in GitHub HTML mode
+ $regex = '/releases/tag/(?:v|V)?([\d.-]+)'
+ }
}
# SourceForge
@@ -216,13 +243,6 @@ $Queue | ForEach-Object {
$reverse = $json.checkver.reverse -and $json.checkver.reverse -eq 'true'
- if ($url -like '*api.github.com/*') { $useGithubAPI = $true }
-
- if ($useGithubAPI -and ($null -ne $GitHubToken)) {
- $url = $url -replace '//(www\.)?github.com/', '//api.github.com/repos/'
- $wc.Headers.Add('Authorization', "token $GitHubToken")
- }
-
$url = substitute $url $substitutions
$state = New-Object psobject @{
@@ -244,7 +264,9 @@ $Queue | ForEach-Object {
}
$wc.Headers.Add('Referer', (strip_filename $url))
+ Register-ObjectEvent $wc downloadDataCompleted -ErrorAction Stop | Out-Null
$wc.DownloadDataAsync($url, $state)
+ $in_progress++
}
function next($er) {
@@ -253,7 +275,6 @@ function next($er) {
}
# wait for all to complete
-$in_progress = $Queue.length
while ($in_progress -gt 0) {
$ev = Wait-Event
Remove-Event $ev.SourceIdentifier
@@ -274,6 +295,8 @@ while ($in_progress -gt 0) {
$expected_ver = $json.version
$ver = $Version
+ $matchesHashtable = @{}
+
if (!$ver) {
if (!$regexp -and $replace) {
next "'replace' requires 're' or 'regex'"
@@ -281,11 +304,19 @@ while ($in_progress -gt 0) {
}
$err = $ev.SourceEventArgs.Error
if ($err) {
- next "$($err.message)`r`nURL $url is not valid"
- continue
+ if (!$script) {
+ next "$($err.message)`r`nURL $url is not valid"
+ continue
+ } else {
+ # Run script despite URL download failure
+ Write-Host "$($err.message)`r`nURL $url is not valid. Falling back to checkver.script ..."
+ }
}
- if ($url) {
+ $page = $null
+ $source = $url
+
+ if ($url -and !$err) {
$ms = New-Object System.IO.MemoryStream
$ms.Write($result, 0, $result.Length)
$ms.Seek(0, 0) | Out-Null
@@ -294,12 +325,17 @@ while ($in_progress -gt 0) {
}
$page = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd()
}
- $source = $url
+
if ($script) {
$page = Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
$source = 'the output of script'
}
+ if ($null -eq $page) {
+ next "couldn't retrieve content from $source"
+ continue
+ }
+
if ($jsonpath) {
# Return only a single value if regex is absent
$noregex = [String]::IsNullOrEmpty($regexp)
@@ -358,7 +394,6 @@ while ($in_progress -gt 0) {
}
if ($match -and $match.Success) {
- $matchesHashtable = @{}
$re.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
$ver = $matchesHashtable['1']
if ($replace) {
diff --git a/bin/scoop.ps1 b/bin/scoop.ps1
index fd2fd41fa7..036a05ba29 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-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') {
- Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline')
+ if ((Test-GitAvailable) -and (Test-Path "$PSScriptRoot\..\.git") -and ((get_config SCOOP_BRANCH 'master') -ne 'master')) {
+ Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('--no-pager', '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-GitAvailable -and (Test-Path "$bucketLoc\.git")) {
+ if ((Test-GitAvailable) -and (Test-Path "$bucketLoc\.git")) {
Write-Host "'$_' bucket:"
- Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline')
+ Invoke-Git -Path $bucketLoc -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
Write-Host ''
}
}
diff --git a/bin/uninstall.ps1 b/bin/uninstall.ps1
index 2e2dc36ea8..9302dd988a 100644
--- a/bin/uninstall.ps1
+++ b/bin/uninstall.ps1
@@ -42,7 +42,7 @@ function do_uninstall($app, $global) {
$architecture = $install.architecture
Write-Output "Uninstalling '$app'"
- Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall
+ Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall
rm_shims $app $manifest $global $architecture
# If a junction was used during install, that will have been used
diff --git a/lib/autoupdate.ps1 b/lib/autoupdate.ps1
index 711f2fd6e5..5ed0387b1d 100644
--- a/lib/autoupdate.ps1
+++ b/lib/autoupdate.ps1
@@ -3,6 +3,8 @@
function format_hash([String] $hash) {
$hash = $hash.toLower()
+ # Workaround for GitHub API:
+ # `"digest": "sha256:"`
if ($hash -like 'sha256:*') {
$hash = $hash.Substring(7) # Remove prefix 'sha256:'
}
@@ -116,6 +118,12 @@ function find_hash_in_json([String] $url, [Hashtable] $substitutions, [String] $
$wc = New-Object Net.Webclient
$wc.Headers.Add('Referer', (strip_filename $url))
$wc.Headers.Add('User-Agent', (Get-UserAgent))
+
+ if (($url -match '^https?://api\.github\.com/.*') -and (Get-GitHubToken)) {
+ $wc.Headers.Add('Authorization', "Bearer $(Get-GitHubToken)")
+ $wc.Headers.Add('X-GitHub-Api-Version', '2022-11-28')
+ }
+
$data = $wc.DownloadData($url)
$ms = New-Object System.IO.MemoryStream
$ms.Write($data, 0, $data.Length)
@@ -209,13 +217,14 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hash = $null
$hashmode = $config.mode
+ $originurl = strip_fragment $url
$basename = [System.Web.HttpUtility]::UrlDecode((url_remote_filename($url)))
$substitutions = $substitutions.Clone()
- $substitutions.Add('$url', (strip_fragment $url))
- $substitutions.Add('$baseurl', (strip_filename (strip_fragment $url)).TrimEnd('/'))
+ $substitutions.Add('$url', $originurl)
+ $substitutions.Add('$baseurl', (strip_filename $originurl).TrimEnd('/'))
$substitutions.Add('$basename', $basename)
- $substitutions.Add('$urlNoExt', (strip_ext (strip_fragment $url)))
+ $substitutions.Add('$urlNoExt', (strip_ext $originurl))
$substitutions.Add('$basenameNoExt', (strip_ext $basename))
debug $substitutions
@@ -297,7 +306,7 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
}
'github' {
$hashfile_url = "https://api.github.com/repos/$($matches['owner'])/$($matches['repo'])/releases"
- $hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $url + "')].digest")
+ $hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $originurl + "')].digest")
}
}
diff --git a/lib/buckets.ps1 b/lib/buckets.ps1
index 87bc02d287..4b29feae52 100644
--- a/lib/buckets.ps1
+++ b/lib/buckets.ps1
@@ -108,12 +108,12 @@ function list_buckets {
$path = Find-BucketDirectory $_ -Root
if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) {
$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
+ $bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aI', '-n', '1') | Get-Date
} else {
$bucket.Source = friendly_path $path
$bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime
}
- $bucket.Manifests = Get-ChildItem "$path\bucket" -Force -Recurse -ErrorAction SilentlyContinue |
+ $bucket.Manifests = Get-ChildItem -Path "$path\bucket" -Filter "*.json" -File -Force -Recurse -ErrorAction SilentlyContinue |
Measure-Object | Select-Object -ExpandProperty Count
$buckets += [PSCustomObject]$bucket
}
@@ -160,7 +160,7 @@ function add_bucket($name, $repo) {
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
return 1
}
- Write-Host 'OK'
+ Write-Host 'OK.'
if (get_config USE_SQLITE_CACHE) {
info 'Updating cache...'
Set-ScoopDB -Path (Get-ChildItem (Find-BucketDirectory $name) -Filter '*.json' -Recurse).FullName
@@ -190,7 +190,7 @@ function new_issue_msg($app, $bucket, $title, $body) {
$url = known_bucket_repo $bucket
$bucket_path = "$bucketsdir\$bucket"
- if (Test-Path $bucket_path) {
+ if ((Test-Path $bucket_path) -and (Test-GitAvailable)) {
$remote = Invoke-Git -Path $bucket_path -ArgumentList @('config', '--get', 'remote.origin.url')
# Support ssh and http syntax
# git@PROVIDER:USER/REPO.git
diff --git a/lib/core.ps1 b/lib/core.ps1
index 7ca7d121e2..63be1a5c4f 100644
--- a/lib/core.ps1
+++ b/lib/core.ps1
@@ -16,8 +16,8 @@ function Get-PESubsystem($filePath) {
} catch {
return -1
} finally {
- $binaryReader.Close()
- $fileStream.Close()
+ if ($null -ne $binaryReader) { $binaryReader.Close() }
+ if ($null -ne $fileStream) { $fileStream.Close() }
}
}
@@ -80,7 +80,7 @@ function Show-DeprecatedWarning {
}
function load_cfg($file) {
- if(!(Test-Path $file)) {
+ if (!(Test-Path $file)) {
return $null
}
@@ -90,20 +90,20 @@ function load_cfg($file) {
$content = [System.IO.File]::ReadAllLines($file)
return ($content | ConvertFrom-Json -ErrorAction Stop)
} catch {
- Write-Host "ERROR loading $file`: $($_.exception.message)"
+ error "loading $file`: $($_.exception.message)"
}
}
function get_config($name, $default) {
$name = $name.ToLowerInvariant()
- if($null -eq $scoopConfig.$name -and $null -ne $default) {
+ if ($null -eq $scoopConfig.$name -and $null -ne $default) {
return $default
}
return $scoopConfig.$name
}
function set_config {
- Param (
+ param (
[ValidateNotNullOrEmpty()]
$name,
$value
@@ -206,9 +206,6 @@ function Complete-ConfigChange {
}
if ($Name -eq 'use_sqlite_cache' -and $Value -eq $true) {
- if ((Get-DefaultArchitecture) -eq 'arm64') {
- abort 'SQLite cache is not supported on ARM64 platform.'
- }
. "$PSScriptRoot\..\lib\database.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
info 'Initializing SQLite cache in progress... This may take a while, please wait.'
@@ -238,11 +235,11 @@ function Invoke-Git {
$ArgumentList = @('-C', $WorkingDirectory) + $ArgumentList
}
- if([String]::IsNullOrEmpty($proxy) -or $proxy -eq 'none') {
+ if ([String]::IsNullOrEmpty($proxy) -or $proxy -eq 'none') {
return & $git @ArgumentList
}
- if($ArgumentList -Match '\b(clone|checkout|pull|fetch|ls-remote)\b') {
+ if ($ArgumentList -match '\b(clone|checkout|pull|fetch|ls-remote)\b') {
$j = Start-Job -ScriptBlock {
# convert proxy setting for git
$proxy = $using:proxy
@@ -262,26 +259,26 @@ function Invoke-Git {
function Invoke-GitLog {
[CmdletBinding()]
- Param (
+ param (
[Parameter(Mandatory, ValueFromPipeline)]
[String]$Path,
[Parameter(Mandatory, ValueFromPipeline)]
[String]$CommitHash,
[String]$Name = ''
)
- Process {
+ 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")
+ 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")
}
}
# helper functions
-function coalesce($a, $b) { if($a) { return $a } $b }
+function coalesce($a, $b) { if ($a) { return $a } $b }
function is_admin {
$admin = [security.principal.windowsbuiltinrole]::administrator
@@ -290,10 +287,10 @@ function is_admin {
}
# messages
-function abort($msg, [int] $exit_code=1) { write-host $msg -f red; exit $exit_code }
-function error($msg) { write-host "ERROR $msg" -f darkred }
-function warn($msg) { write-host "WARN $msg" -f darkyellow }
-function info($msg) { write-host "INFO $msg" -f darkgray }
+function abort($msg, [int] $exit_code = 1) { Write-Host $msg -f red; exit $exit_code }
+function error($msg) { Write-Host "ERROR $msg" -f darkred }
+function warn($msg) { Write-Host "WARN $msg" -f darkyellow }
+function info($msg) { Write-Host "INFO $msg" -f darkgray }
function debug($obj) {
if ((get_config DEBUG $false) -ine 'true' -and $env:SCOOP_DEBUG -ine 'true') {
return
@@ -303,14 +300,14 @@ function debug($obj) {
$param = $MyInvocation.Line.Replace($MyInvocation.InvocationName, '').Trim()
$msg = $obj | Out-String -Stream
- if($null -eq $obj -or $null -eq $msg) {
+ if ($null -eq $obj -or $null -eq $msg) {
Write-Host "$prefix $param = " -f DarkCyan -NoNewline
Write-Host '$null' -f DarkYellow -NoNewline
Write-Host " -> $($MyInvocation.PSCommandPath):$($MyInvocation.ScriptLineNumber):$($MyInvocation.OffsetInLine)" -f DarkGray
return
}
- if($msg.GetType() -eq [System.Object[]]) {
+ if ($msg.GetType() -eq [System.Object[]]) {
Write-Host "$prefix $param ($($obj.GetType()))" -f DarkCyan -NoNewline
Write-Host " -> $($MyInvocation.PSCommandPath):$($MyInvocation.ScriptLineNumber):$($MyInvocation.OffsetInLine)" -f DarkGray
$msg | Where-Object { ![String]::IsNullOrWhiteSpace($_) } |
@@ -323,18 +320,18 @@ function debug($obj) {
Write-Host " -> $($MyInvocation.PSCommandPath):$($MyInvocation.ScriptLineNumber):$($MyInvocation.OffsetInLine)" -f DarkGray
}
}
-function success($msg) { write-host $msg -f darkgreen }
+function success($msg) { Write-Host $msg -f darkgreen }
function filesize($length) {
$gb = [math]::pow(2, 30)
$mb = [math]::pow(2, 20)
$kb = [math]::pow(2, 10)
- if($length -gt $gb) {
+ if ($length -gt $gb) {
"{0:n1} GB" -f ($length / $gb)
- } elseif($length -gt $mb) {
+ } elseif ($length -gt $mb) {
"{0:n1} MB" -f ($length / $mb)
- } elseif($length -gt $kb) {
+ } elseif ($length -gt $kb) {
"{0:n1} KB" -f ($length / $kb)
} else {
if ($null -eq $length) {
@@ -345,7 +342,7 @@ function filesize($length) {
}
# dirs
-function basedir($global) { if($global) { return $globaldir } $scoopdir }
+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" }
@@ -353,7 +350,7 @@ function appdir($app, $global) { "$(appsdir $global)\$app" }
function versiondir($app, $version, $global) { "$(appdir $app $global)\$version" }
function currentdir($app, $global) {
- if (get_config NO_JUNCTION) {
+ if ((get_config NO_JUNCTION) -and ($app -ne 'scoop')) {
$version = Select-CurrentVersion -App $app -Global:$global
} else {
$version = 'current'
@@ -441,14 +438,14 @@ function Get-AppFilePath {
return $null
}
-Function Test-CommandAvailable {
+function Test-CommandAvailable {
param (
[String]$Name
)
- Return [Boolean](Get-Command $Name -ErrorAction Ignore)
+ return [Boolean](Get-Command $Name -ErrorAction Ignore)
}
-Function Test-GitAvailable {
+function Test-GitAvailable {
return [Boolean](Get-HelperPath -Helper Git)
}
@@ -483,9 +480,9 @@ function Get-HelperPath {
}
}
'Dark' {
- $HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe'
+ $HelperPath = Get-AppFilePath 'dark' 'dark.exe'
if ([String]::IsNullOrEmpty($HelperPath)) {
- $HelperPath = Get-AppFilePath 'dark' 'dark.exe'
+ $HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe'
}
}
'Aria2' { $HelperPath = Get-AppFilePath 'aria2' 'aria2c.exe' }
@@ -555,7 +552,9 @@ function app_status($app, $global) {
$status.hold = ($install_info.hold -eq $true)
$deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
- $status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse).FullName
+ if (Test-Path $deprecated_dir) {
+ $status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse).FullName
+ }
$manifest = manifest $app $install_info.bucket $install_info.url
$status.removed = (!$manifest)
@@ -588,14 +587,14 @@ function app_status($app, $global) {
}
function appname_from_url($url) {
- (split-path $url -leaf) -replace '.json$', ''
+ (Split-Path $url -Leaf) -replace '.json$', ''
}
# paths
-function fname($path) { split-path $path -leaf }
+function fname($path) { Split-Path $path -Leaf }
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
-function strip_fragment($url) { $url -replace (new-object uri $url).fragment }
+function strip_fragment($url) { $url -replace (New-Object uri $url).fragment }
function ensure($dir) {
if (!(Test-Path -Path $dir)) {
New-Item -Path $dir -ItemType Directory | Out-Null
@@ -622,7 +621,14 @@ function Get-AbsolutePath {
$Path
)
process {
- return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
+ $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
+ if ($resolvedPath -match '[\\/]$') {
+ $root = [System.IO.Path]::GetPathRoot($resolvedPath)
+ if ($resolvedPath -ine $root) {
+ $resolvedPath = $resolvedPath.TrimEnd([char[]]@('\', '/'))
+ }
+ }
+ return $resolvedPath
}
}
@@ -718,7 +724,7 @@ function Invoke-ExternalCommand {
# ref-1: https://learn.microsoft.com/en-us/powershell/scripting/learn/experimental-features?view=powershell-7.4#psnativecommandargumentpassing
# ref-2: https://nsis.sourceforge.io/Docs/Chapter3.html
$LegacyCommand = $FilePath -match '^((cmd|cscript|find|sqlcmd|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$' -or
- ($ArgumentList -match '^/S$|^/D=[A-Z]:[\\/].*$').Length -eq 2
+ ($ArgumentList -match '^/S$|^/D=[A-Z]:[\\/].*$').Length -eq 2
$SupportArgumentList = $Process.StartInfo.PSObject.Properties.Name -contains 'ArgumentList'
if ((-not $LegacyCommand) -and $SupportArgumentList) {
# ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+)
@@ -743,7 +749,7 @@ function Invoke-ExternalCommand {
[void]$Process.Start()
} catch {
if ($Activity) {
- Write-Host "error." -ForegroundColor DarkRed
+ Write-Host "Error." -ForegroundColor DarkRed
}
error $_.Exception.Message
return $false
@@ -762,20 +768,20 @@ function Invoke-ExternalCommand {
if ($Process.ExitCode -ne 0) {
if ($ContinueExitCodes -and ($ContinueExitCodes.ContainsKey($Process.ExitCode))) {
if ($Activity) {
- Write-Host "done." -ForegroundColor DarkYellow
+ Write-Host "Done." -ForegroundColor DarkYellow
}
warn $ContinueExitCodes[$Process.ExitCode]
return $true
} else {
if ($Activity) {
- Write-Host "error." -ForegroundColor DarkRed
+ Write-Host "Error." -ForegroundColor DarkRed
}
error "Exit code was $($Process.ExitCode)!"
return $false
}
}
if ($Activity) {
- Write-Host "done." -ForegroundColor Green
+ Write-Host "Done." -ForegroundColor Green
}
return $true
}
@@ -793,8 +799,7 @@ function isFileLocked([string]$path) {
$stream.Close()
}
return $false
- }
- catch {
+ } catch {
# file is locked by a process.
return $true
}
@@ -819,7 +824,7 @@ function movedir($from, $to) {
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$proc.WaitForExit()
- if($proc.ExitCode -ge 8) {
+ if ($proc.ExitCode -ge 8) {
debug $stdoutTask.Result
throw "Could not find '$(fname $from)'! (error $($proc.ExitCode))"
}
@@ -908,7 +913,8 @@ function shim($path, $global, $name, $arg) {
}
$target_subsystem = Get-PESubsystem $resolved_path
- if ($target_subsystem -eq 2) { # we only want to make shims GUI
+ if ($target_subsystem -eq 2) {
+ # we only want to make shims GUI
Write-Output "Making $shim.exe a GUI binary."
Set-PESubsystem "$shim.exe" $target_subsystem | Out-Null
}
@@ -1121,30 +1127,30 @@ function Confirm-InstallationStatus {
}
function wraptext($text, $width) {
- if(!$width) { $width = $host.ui.rawui.buffersize.width };
+ if (!$width) { $width = $host.ui.rawui.buffersize.width }
$width -= 1 # be conservative: doesn't seem to print the last char
$text -split '\r?\n' | ForEach-Object {
$line = ''
$_ -split ' ' | ForEach-Object {
- if($line.length -eq 0) { $line = $_ }
- elseif($line.length + $_.length + 1 -le $width) { $line += " $_" }
- else { $lines += ,$line; $line = $_ }
+ if ($line.length -eq 0) { $line = $_ }
+ elseif ($line.length + $_.length + 1 -le $width) { $line += " $_" }
+ else { $lines += , $line; $line = $_ }
}
- $lines += ,$line
+ $lines += , $line
}
$lines -join "`n"
}
function pluralize($count, $singular, $plural) {
- if($count -eq 1) { $singular } else { $plural }
+ if ($count -eq 1) { $singular } else { $plural }
}
# convert list of apps to list of ($app, $global) tuples
function applist($apps, $global) {
- if(!$apps) { return @() }
- return ,@($apps | ForEach-Object { ,@($_, $global) })
+ if (!$apps) { return @() }
+ return , @($apps | ForEach-Object { , @($_, $global) })
}
function parse_app([string]$app) {
@@ -1156,10 +1162,10 @@ function parse_app([string]$app) {
}
function show_app($app, $bucket, $version) {
- if($bucket) {
+ if ($bucket) {
$app = "$bucket/$app"
}
- if($version) {
+ if ($version) {
$app = "$app@$version"
}
return $app
@@ -1205,11 +1211,12 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
$newentity = $entity.PSObject.Copy()
switch ($entity.GetType().Name) {
'String' {
- $params.GetEnumerator() | ForEach-Object {
- if ($regexEscape -eq $false -or $null -eq $_.Value) {
- $newentity = $newentity.Replace($_.Name, $_.Value)
+ $params.Keys | Sort-Object Length -Descending | ForEach-Object {
+ $value = $params[$_]
+ if ($regexEscape -eq $false -or $null -eq $value) {
+ $newentity = $newentity.Replace($_, $value)
} else {
- $newentity = $newentity.Replace($_.Name, [Regex]::Escape($_.Value))
+ $newentity = $newentity.Replace($_, [Regex]::Escape($value))
}
}
}
@@ -1264,7 +1271,7 @@ $configHome = $env:XDG_CONFIG_HOME, "$env:USERPROFILE\.config" | Select-Object -
$configFile = "$configHome\scoop\config.json"
# Check if it's the expected install path for scoop: /apps/scoop/current
$coreRoot = Split-Path $PSScriptRoot
-$pathExpected = ($coreRoot -replace '\\','/') -like '*apps/scoop/current*'
+$pathExpected = ($coreRoot -replace '\\', '/') -like '*apps/scoop/current*'
if ($pathExpected) {
# Portable config is located in root directory:
# .\current\scoop\apps\\config.json <- a reversed path
diff --git a/lib/database.ps1 b/lib/database.ps1
index 912db11c31..2af430fd33 100644
--- a/lib/database.ps1
+++ b/lib/database.ps1
@@ -4,10 +4,11 @@
.SYNOPSIS
Get SQLite .NET driver
.DESCRIPTION
- Download and extract the SQLite .NET driver from NuGet.
+ Download and extract the SQLite .NET driver and SQLite precompiled binaries.
+ The SQLite version is automatically detected from the download page.
.PARAMETER Version
System.String
- The version of the SQLite .NET driver to download.
+ The version of the System.Data.SQLite NuGet package to download. (require version 2.0.0 or higher)
.INPUTS
None
.OUTPUTS
@@ -16,21 +17,44 @@
#>
function Get-SQLite {
param (
- [string]$Version = '1.0.118'
+ [string]$Version = '2.0.2'
)
- # Install SQLite
try {
- Write-Host "Downloading SQLite $Version..." -ForegroundColor DarkYellow
- $sqlitePkgPath = "$env:TEMP\sqlite.zip"
+ $sqliteNetPath = "$env:TEMP\sqlite.net.zip"
+ $sqliteDllPath = "$env:TEMP\sqlite.dll.zip"
$sqliteTempPath = "$env:TEMP\sqlite"
$sqlitePath = "$PSScriptRoot\..\supporting\sqlite"
- Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/stub.system.data.sqlite.core.netframework/$version/stub.system.data.sqlite.core.netframework.$version.nupkg" -OutFile $sqlitePkgPath
- Write-Host "Extracting SQLite $Version..." -ForegroundColor DarkYellow -NoNewline
- Expand-Archive -Path $sqlitePkgPath -DestinationPath $sqliteTempPath -Force
- New-Item -Path $sqlitePath -ItemType Directory -Force | Out-Null
- Move-Item -Path "$sqliteTempPath\build\net451\*", "$sqliteTempPath\lib\net451\System.Data.SQLite.dll" -Destination $sqlitePath -Force
- Remove-Item -Path $sqlitePkgPath, $sqliteTempPath -Recurse -Force
- Write-Host ' Done' -ForegroundColor DarkYellow
+
+ $arch = switch (Get-DefaultArchitecture) {
+ '32bit' { 'x86' }
+ '64bit' { 'x64' }
+ 'arm64' { 'arm64' }
+ default { Write-Warning "Unknown architecture, using x64 as fallback"; 'x64' }
+ }
+
+ Write-Host "Downloading System.Data.SQLite $Version..." -ForegroundColor DarkYellow
+ Invoke-WebRequest -Uri "https://globalcdn.nuget.org/packages/system.data.sqlite.$Version.nupkg" -OutFile $sqliteNetPath
+
+ $downloadPage = Invoke-WebRequest -Uri 'https://sqlite.org/download.html' -UseBasicParsing
+ if ($downloadPage.Content -match '(?s)') {
+ $productData = $Matches[1] | ConvertFrom-Csv
+ } else {
+ throw "Failed to parse SQLite download page product data"
+ }
+ $matchRow = $productData | Where-Object { $_.'RELATIVE-URL' -match "sqlite-dll-win-$arch-" }
+ if (-not $matchRow) {
+ throw "SQLite DLL for architecture $arch not found"
+ }
+ Write-Host "Downloading SQLite DLL $($matchRow.VERSION)..." -ForegroundColor DarkYellow
+ Invoke-WebRequest -Uri "https://sqlite.org/$($matchRow.'RELATIVE-URL')" -OutFile $sqliteDllPath
+
+ Write-Host "Extracting libraries... " -ForegroundColor DarkYellow -NoNewline
+ $sqliteNetPath, $sqliteDllPath | Expand-Archive -DestinationPath $sqliteTempPath -Force
+ $null = New-Item -Path "$sqlitePath\$arch" -ItemType Directory -Force
+ Move-Item -Path "$sqliteTempPath\lib\netstandard2.0\System.Data.SQLite.dll" -Destination $sqlitePath -Force
+ Move-Item -Path "$sqliteTempPath\sqlite3.dll" -Destination "$sqlitePath\$arch\e_sqlite3.dll" -Force
+ Remove-Item -Path $sqliteNetPath, $sqliteDllPath, $sqliteTempPath -Recurse -Force
+ Write-Host 'Done.' -ForegroundColor DarkYellow
return $true
} catch {
return $false
@@ -219,9 +243,9 @@ function Set-ScoopDB {
<#
.SYNOPSIS
- Select Scoop database item(s).
+ Find Scoop database item(s).
.DESCRIPTION
- Select item(s) from the Scoop SQLite database.
+ Find item(s) from the Scoop SQLite database.
The pattern is matched against the name, binaries, and shortcuts columns for apps.
.PARAMETER Pattern
System.String
@@ -233,9 +257,9 @@ function Set-ScoopDB {
System.String
.OUTPUTS
System.Data.DataTable
- The selected database item(s).
+ The found database item(s).
#>
-function Select-ScoopDBItem {
+function Find-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
@@ -253,7 +277,6 @@ function Select-ScoopDBItem {
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
$result = New-Object System.Data.DataTable
$dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')"
- $dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket"
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
@@ -264,9 +287,10 @@ function Select-ScoopDBItem {
[void]$dbAdapter.Fill($result)
}
end {
+ $dbCommand.Dispose()
$dbAdapter.Dispose()
$db.Dispose()
- return $result
+ return Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket')
}
}
@@ -311,8 +335,6 @@ function Get-ScoopDBItem {
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
if ($Version) {
$dbQuery += ' AND version = @Version'
- } else {
- $dbQuery += ' ORDER BY version DESC LIMIT 1'
}
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
@@ -322,16 +344,110 @@ function Get-ScoopDBItem {
process {
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
- $dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null
+ if ($Version) {
+ $dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null
+ }
[void]$dbAdapter.Fill($result)
}
end {
+ $dbCommand.Dispose()
$dbAdapter.Dispose()
$db.Dispose()
- return $result
+ # With $Version, the PRIMARY KEY guarantees at most one row; without it, the
+ # query is already limited to one name+bucket pair, so selecting latest needs no -GroupBy.
+ if ($Version) {
+ return $result
+ }
+
+ return Select-LatestScoopDBRow -Table $result
}
}
+<#
+.SYNOPSIS
+ Get the latest row from a set of Scoop database rows.
+.DESCRIPTION
+ Compares the `version` property of each row semantically and returns the
+ latest row. Returns `$null` when no rows are provided.
+.PARAMETER Rows
+ System.Object[]
+ The rows to evaluate for the latest version. Each row must have a `version` property.
+.INPUTS
+ System.Object[]
+.OUTPUTS
+ System.Object
+ The latest row based on semantic versioning, or `$null` if no rows are provided.
+#>
+function Get-LatestScoopDBRow {
+ param(
+ [Parameter(Mandatory)]
+ [AllowEmptyCollection()]
+ [object[]]
+ $Rows
+ )
+
+ if (-not $Rows -or $Rows.Count -eq 0) {
+ return $null
+ }
+
+ if (-not (Get-Command Compare-Version -ErrorAction Ignore)) {
+ . "$PSScriptRoot\versions.ps1"
+ }
+
+ $latest = $Rows[0]
+ for ($i = 1; $i -lt $Rows.Count; $i++) {
+ $row = $Rows[$i]
+ if ((Compare-Version -ReferenceVersion $latest.version -DifferenceVersion $row.version) -gt 0) {
+ $latest = $row
+ }
+ }
+
+ return $latest
+}
+
+<#
+.SYNOPSIS
+ Return the semantically latest row or rows from a Scoop database result set.
+.DESCRIPTION
+ Clones the schema of `Table` and imports the latest row per group when
+ `GroupBy` is supplied, or the single latest row across the whole table when
+ it is omitted. Returns an empty cloned table when the source table has no rows.
+.PARAMETER Table
+ System.Data.DataTable
+ The source table returned from a Scoop database query.
+.PARAMETER GroupBy
+ System.String[]
+ Optional column names used to group rows before semantic latest-row selection.
+.OUTPUTS
+ System.Data.DataTable
+ A cloned table containing the latest matching row for each requested scope.
+#>
+function Select-LatestScoopDBRow {
+ param(
+ [Parameter(Mandatory)]
+ [System.Data.DataTable]
+ $Table,
+ [string[]]
+ $GroupBy
+ )
+
+ $latestRows = $Table.Clone()
+ $rows = @($Table.Rows)
+ if ($rows.Count -eq 0) {
+ return $latestRows
+ }
+
+ if ($GroupBy -and $GroupBy.Count -gt 0) {
+ foreach ($group in ($rows | Group-Object -Property $GroupBy)) {
+ $latestRows.ImportRow((Get-LatestScoopDBRow -Rows @($group.Group)))
+ }
+ } else {
+ $latestRows.ImportRow((Get-LatestScoopDBRow -Rows $rows))
+ }
+
+ return $latestRows
+}
+
<#
.SYNOPSIS
Remove Scoop database item(s).
diff --git a/lib/decompress.ps1 b/lib/decompress.ps1
index 3f174bf0c3..c25e46f5e3 100644
--- a/lib/decompress.ps1
+++ b/lib/decompress.ps1
@@ -52,11 +52,9 @@ function Invoke-Extraction {
DestinationPath = Join-Path $Path $extractTo[$extracted]
ExtractDir = $extractDir[$extracted]
}
- Write-Host 'Extracting ' -NoNewline
- Write-Host $(url_remote_filename $uri[$i]) -ForegroundColor Cyan -NoNewline
- Write-Host ' ... ' -NoNewline
+ Write-Host "Extracting $([char]0x1b)[36m$(url_remote_filename $uri[$i])$([char]0x1b)[0m... " -NoNewline
& $extractFn @fnArgs -Removal
- Write-Host 'done.' -ForegroundColor Green
+ Write-Host 'Done.' -ForegroundColor Green
$extracted++
}
}
diff --git a/lib/download.ps1 b/lib/download.ps1
index 70641cca7f..ab74d92652 100644
--- a/lib/download.ps1
+++ b/lib/download.ps1
@@ -23,7 +23,8 @@ function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
} catch {
Write-Host -ForegroundColor DarkRed $_
- abort "URL $url is not valid"
+ error "URL $url is not valid"
+ abort $(new_issue_msg $app $bucket 'download failed')
}
if ($check_hash) {
@@ -390,9 +391,7 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
}
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
- Write-Host 'Loading ' -NoNewline
- Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
- Write-Host ' from cache.'
+ Write-Host "Loading $([char]0x1b)[36m$(url_remote_filename $url)$([char]0x1b)[0m from cache."
} else {
$download_finished = $false
# create aria2 input file content
@@ -424,7 +423,7 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
# handle aria2 console output
- Write-Host 'Starting download with aria2 ...'
+ Write-Host 'Starting download with aria2...'
# Set console output encoding to UTF8 for non-ASCII characters printing
$oriConsoleEncoding = [Console]::OutputEncoding
@@ -454,12 +453,18 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
Write-Host ''
if ($lastexitcode -gt 0) {
+ if (get_config ARIA2-FALLBACK-DISABLED) {
+ error "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
+ error $urlstxt_content
+ error $aria2
+ abort $(new_issue_msg $app $bucket 'download via aria2 failed')
+ }
+
warn "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
warn $urlstxt_content
warn $aria2
- warn $(new_issue_msg $app $bucket "download via aria2 failed")
- Write-Host "Fallback to default downloader ..."
+ Write-Host 'Fallback to default downloader...'
try {
foreach ($url in $urls) {
@@ -467,7 +472,8 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
}
} catch {
Write-Host $_ -ForegroundColor DarkRed
- abort "URL $url is not valid"
+ error "URL $url is not valid"
+ abort $(new_issue_msg $app $bucket 'download failed')
}
}
@@ -723,9 +729,7 @@ function check_hash($file, $hash, $app_name) {
return $true, $null
}
- Write-Host 'Checking hash of ' -NoNewline
- Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
- Write-Host ' ... ' -NoNewline
+ Write-Host "Checking hash of $([char]0x1b)[36m$(url_remote_filename $url)$([char]0x1b)[0m... " -NoNewline
$algorithm, $expected = get_hash $hash
if ($null -eq $algorithm) {
return $false, "Hash type '$algorithm' isn't supported."
@@ -747,7 +751,7 @@ function check_hash($file, $hash, $app_name) {
}
return $false, $msg
}
- Write-Host 'ok.' -f Green
+ Write-Host 'OK.' -f Green
return $true, $null
}
diff --git a/lib/getopt.ps1 b/lib/getopt.ps1
index 2de3ac6c0a..0ba80965ea 100644
--- a/lib/getopt.ps1
+++ b/lib/getopt.ps1
@@ -9,10 +9,10 @@
# a parameter should end with '='
# returns @(opts hash, remaining_args array, error string)
# NOTES:
-# The first "--" in $argv, if any, will terminate all options; any
-# 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)
+# The first "--" or "--%" in $argv, if any, will terminate all options; any
+# following arguments are treated as non-option arguments, even if
+# they begin with a hyphen. The terminator token itself ("--" or "--%")
+# will not be included. (POSIX-compatible)
function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) {
$opts = @{}; $rem = @()
@@ -32,7 +32,7 @@ function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) {
if ($arg -is [Int]) { $rem += $arg; continue }
if ($arg -is [Decimal]) { $rem += $arg; continue }
- if ($arg -eq '--') {
+ if ($arg -eq '--' -or $arg -eq '--%') {
if ($i -lt $argv.Length - 1) {
$rem += $argv[($i + 1)..($argv.Length - 1)]
}
diff --git a/lib/install.ps1 b/lib/install.ps1
index 56cfdac924..78acdd252b 100644
--- a/lib/install.ps1
+++ b/lib/install.ps1
@@ -165,9 +165,9 @@ function Invoke-HookScript {
$script = $script.script
}
if ($script) {
- Write-Host "Running $HookType script..." -NoNewline
+ Write-Host "Running $HookType script... " -NoNewline
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
- Write-Host 'done.' -ForegroundColor Green
+ Write-Host 'Done.' -ForegroundColor Green
}
}
@@ -285,7 +285,7 @@ function ensure_install_dir_not_in_path($dir, $global) {
$fixed, $removed = find_dir_or_subdir $path "$dir"
if ($removed) {
- $removed | ForEach-Object { "Installer added '$(friendly_path $_)' to path. Removing." }
+ $removed | ForEach-Object { Write-Output "Installer added '$(friendly_path $_)' to path. Removing." }
Set-EnvVar -Name 'PATH' -Value $fixed -Global:$global
}
@@ -338,6 +338,7 @@ function env_set($manifest, $global, $arch) {
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
$name = $_.Name
$val = $ExecutionContext.InvokeCommand.ExpandString($env_set.$($name))
+ Write-Output "Setting $(if ($global) {'system'} else {'user'}) environment variable: $([char]0x1b)[34m$name$([char]0x1b)[0m = $([char]0x1b)[35m$val$([char]0x1b)[0m"
Set-EnvVar -Name $name -Value $val -Global:$global
Set-Content env:\$name $val
}
@@ -348,6 +349,7 @@ function env_rm($manifest, $global, $arch) {
if ($env_set) {
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
$name = $_.Name
+ Write-Output "Removing $(if ($global) {'system'} else {'user'}) environment variable: $([char]0x1b)[34m$name$([char]0x1b)[0m"
Set-EnvVar -Name $name -Value $null -Global:$global
if (Test-Path env:\$name) { Remove-Item env:\$name }
}
@@ -359,6 +361,7 @@ function show_notes($manifest, $dir, $original_dir, $persist_dir) {
Write-Output 'Notes'
Write-Output '-----'
Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }))
+ Write-Output '-----'
}
}
@@ -419,7 +422,7 @@ function show_suggestions($suggested) {
}
if (!$fulfilled) {
- Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'."
+ Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." -ForegroundColor DarkYellow
}
}
}
@@ -554,7 +557,7 @@ function test_running_process($app, $global) {
# Required to handle docker/for-win#12240
function New-DirectoryJunction($source, $target) {
# test if this script is being executed inside a docker container
- if (Get-Service -Name cexecsvc -ErrorAction SilentlyContinue) {
+ if (Get-Service -Name cexecsvc -ErrorAction Ignore) {
cmd.exe /d /c "mklink /j `"$source`" `"$target`""
} else {
New-Item -Path $source -ItemType Junction -Value $target
diff --git a/lib/manifest.ps1 b/lib/manifest.ps1
index 29c898b6a7..59e2bd6019 100644
--- a/lib/manifest.ps1
+++ b/lib/manifest.ps1
@@ -18,8 +18,8 @@ function url_manifest($url) {
$wc.Headers.Add('User-Agent', (Get-UserAgent))
$data = $wc.DownloadData($url)
$str = (Get-Encoding($wc)).GetString($data)
- } catch [system.management.automation.methodinvocationexception] {
- warn "error: $($_.exception.innerexception.message)"
+ } catch [System.Management.Automation.MethodInvocationException] {
+ error $_.Exception.InnerException.Message
} catch {
throw
}
@@ -69,7 +69,7 @@ function Get-Manifest($app) {
$manifest = manifest $app $bucket
if (!$manifest) {
$deprecated_dir = (Find-BucketDirectory -Name $bucket -Root) + '\deprecated'
- $manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse).FullName
+ $manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
}
}
}
diff --git a/libexec/scoop-alias.ps1 b/libexec/scoop-alias.ps1
index 677b9337f7..2b0545abe4 100644
--- a/libexec/scoop-alias.ps1
+++ b/libexec/scoop-alias.ps1
@@ -40,7 +40,7 @@ if ($SubCommand -notin $SubCommands) {
}
$opt, $other, $err = getopt $Args 'v' 'verbose'
-if ($err) { "scoop alias: $err"; exit 1 }
+if ($err) { error "scoop alias: $err"; exit 1 }
$name, $command, $description = $other
$verbose = $opt.v -or $opt.verbose
diff --git a/libexec/scoop-bucket.ps1 b/libexec/scoop-bucket.ps1
index d3b7728559..39d8ccf8da 100644
--- a/libexec/scoop-bucket.ps1
+++ b/libexec/scoop-bucket.ps1
@@ -34,14 +34,14 @@ $usage_rm = 'usage: scoop bucket rm '
switch ($cmd) {
'add' {
if (!$name) {
- ' missing'
+ error ' missing'
$usage_add
exit 1
}
if (!$repo) {
$repo = known_bucket_repo $name
if (!$repo) {
- "Unknown bucket '$name'. Try specifying ."
+ error "Unknown bucket '$name'. Try specifying ."
$usage_add
exit 1
}
@@ -51,7 +51,7 @@ switch ($cmd) {
}
'rm' {
if (!$name) {
- ' missing'
+ error ' missing'
$usage_rm
exit 1
}
@@ -73,7 +73,7 @@ switch ($cmd) {
exit 0
}
default {
- "scoop bucket: cmd '$cmd' not supported"
+ error "scoop bucket: cmd '$cmd' not supported"
my_usage
exit 1
}
diff --git a/libexec/scoop-cache.ps1 b/libexec/scoop-cache.ps1
index 30e8354fca..e81a626db7 100644
--- a/libexec/scoop-cache.ps1
+++ b/libexec/scoop-cache.ps1
@@ -35,7 +35,7 @@ function cacheshow($app) {
function cacheremove($app) {
if (!$app) {
- 'ERROR: missing'
+ error ' missing'
my_usage
exit 1
} elseif ($app -eq '*' -or $app -eq '-a' -or $app -eq '--all') {
diff --git a/libexec/scoop-cleanup.ps1 b/libexec/scoop-cleanup.ps1
index 6ce40a211f..3ea5194d0f 100644
--- a/libexec/scoop-cleanup.ps1
+++ b/libexec/scoop-cleanup.ps1
@@ -16,15 +16,15 @@
. "$PSScriptRoot\..\lib\install.ps1" # persist related
$opt, $apps, $err = getopt $args 'agk' 'all', 'global', 'cache'
-if ($err) { "scoop cleanup: $err"; exit 1 }
+if ($err) { error "scoop cleanup: $err"; exit 1 }
$global = $opt.g -or $opt.global
$cache = $opt.k -or $opt.cache
$all = $opt.a -or $opt.all
-if (!$apps -and !$all) { 'ERROR: missing'; my_usage; exit 1 }
+if (!$apps -and !$all) { error ' missing'; my_usage; exit 1 }
if ($global -and !(is_admin)) {
- 'ERROR: you need admin rights to cleanup global apps'; exit 1
+ error 'you need admin rights to cleanup global apps'; exit 1
}
function cleanup($app, $global, $verbose, $cache) {
diff --git a/libexec/scoop-config.ps1 b/libexec/scoop-config.ps1
index 6007bd6434..954b0b6a1c 100644
--- a/libexec/scoop-config.ps1
+++ b/libexec/scoop-config.ps1
@@ -132,6 +132,9 @@
# aria2-warning-enabled: $true|$false
# Disable Aria2c warning which is shown while downloading.
#
+# aria2-fallback-disabled: $true|$false
+# Disable automatic fallback to the default downloader when Aria2c download fails.
+#
# aria2-retry-wait: 2
# Number of seconds to wait between retries.
# See: 'https://aria2.github.io/manual/en/html/aria2c.html#cmdoption-retry-wait'
diff --git a/libexec/scoop-download.ps1 b/libexec/scoop-download.ps1
index 996cb4e8e6..76c894834b 100644
--- a/libexec/scoop-download.ps1
+++ b/libexec/scoop-download.ps1
@@ -25,6 +25,7 @@
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1"
+. "$PSScriptRoot\..\lib\install.ps1" # 'nightly_version'
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}
@@ -41,6 +42,8 @@ try {
abort "ERROR: $_"
}
+$apps = $apps | Select-Object -Unique
+
if (!$apps) { error ' missing'; my_usage; exit 1 }
if (is_scoop_outdated) {
@@ -99,6 +102,8 @@ foreach ($curr_app in $apps) {
continue
}
+ $dl_failure = $false
+
if(Test-Aria2Enabled) {
Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash
} else {
@@ -108,6 +113,7 @@ foreach ($curr_app in $apps) {
} catch {
write-host -f darkred $_
error "URL $url is not valid"
+ error $(new_issue_msg $app $bucket 'download failed')
$dl_failure = $true
continue
}
@@ -127,6 +133,7 @@ foreach ($curr_app in $apps) {
warn 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
error (new_issue_msg $app $bucket "hash check failed")
+ $dl_failure = $true
continue
}
} else {
diff --git a/libexec/scoop-help.ps1 b/libexec/scoop-help.ps1
index 54ed8a9319..18a67fa9b1 100644
--- a/libexec/scoop-help.ps1
+++ b/libexec/scoop-help.ps1
@@ -27,17 +27,17 @@ function print_summaries {
$commands = commands
-if(!($cmd)) {
+if (!($cmd)) {
Write-Host "Usage: scoop []
Available commands are listed below.
Type 'scoop help ' to get more help for a specific command."
print_summaries
-} elseif($commands -contains $cmd) {
+} elseif ($commands -contains $cmd) {
print_help $cmd
} else {
- warn "scoop help: no such command '$cmd'"
+ error "scoop help: no such command '$cmd'"
exit 1
}
diff --git a/libexec/scoop-hold.ps1 b/libexec/scoop-hold.ps1
index 504f20a849..48f8304c9a 100644
--- a/libexec/scoop-hold.ps1
+++ b/libexec/scoop-hold.ps1
@@ -15,7 +15,7 @@
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
$opt, $apps, $err = getopt $args 'g' 'global'
-if ($err) { "scoop hold: $err"; exit 1 }
+if ($err) { error "scoop hold: $err"; exit 1 }
$global = $opt.g -or $opt.global
diff --git a/libexec/scoop-info.ps1 b/libexec/scoop-info.ps1
index 1322e33a63..1c147e17c0 100644
--- a/libexec/scoop-info.ps1
+++ b/libexec/scoop-info.ps1
@@ -122,7 +122,7 @@ if ($manifest.depends) {
if (Test-Path $manifest_file) {
if (Get-Command git -ErrorAction Ignore) {
- $gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#'
+ $gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aI#%an', $manifest_file) 2> $null) -Split '#'
}
if ($gitinfo) {
$item.'Updated at' = $gitinfo[0] | Get-Date
diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1
index 0fadcff09d..6af199a8b8 100644
--- a/libexec/scoop-install.ps1
+++ b/libexec/scoop-install.ps1
@@ -44,7 +44,7 @@ if (get_config USE_SQLITE_CACHE) {
}
$opt, $apps, $err = getopt $args 'giksua:' 'global', 'independent', 'no-cache', 'skip-hash-check', 'no-update-scoop', 'arch='
-if ($err) { "scoop install: $err"; exit 1 }
+if ($err) { error "scoop install: $err"; exit 1 }
$global = $opt.g -or $opt.global
$check_hash = !($opt.s -or $opt.'skip-hash-check')
diff --git a/libexec/scoop-list.ps1 b/libexec/scoop-list.ps1
index f757146d47..b257823f0a 100644
--- a/libexec/scoop-list.ps1
+++ b/libexec/scoop-list.ps1
@@ -17,7 +17,7 @@ $global = installed_apps $true | ForEach-Object { @{ name = $_; global = $true }
$apps = @($local) + @($global)
if (-not $apps) {
- Write-Host "There aren't any apps installed."
+ warn "There aren't any apps installed."
exit 1
}
@@ -48,7 +48,10 @@ $apps | Where-Object { !$query -or ($_.name -match $query) } | ForEach-Object {
$item.Updated = $updated
$info = @()
- if ((app_status $app $global).deprecated) { $info += 'Deprecated package'}
+ $deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
+ if ((Test-Path $deprecated_dir) -and (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse)) {
+ $info += 'Deprecated package'
+ }
if ($global) { $info += 'Global install' }
if (failed $app $global) { $info += 'Install failed' }
if ($install_info.hold) { $info += 'Held package' }
diff --git a/libexec/scoop-reset.ps1 b/libexec/scoop-reset.ps1
index 512540e8e6..1dd000a373 100644
--- a/libexec/scoop-reset.ps1
+++ b/libexec/scoop-reset.ps1
@@ -14,7 +14,7 @@
. "$PSScriptRoot\..\lib\shortcuts.ps1"
$opt, $apps, $err = getopt $args 'a' 'all'
-if($err) { "scoop reset: $err"; exit 1 }
+if($err) { error "scoop reset: $err"; exit 1 }
$all = $opt.a -or $opt.all
if(!$apps -and !$all) { error ' missing'; my_usage; exit 1 }
diff --git a/libexec/scoop-search.ps1 b/libexec/scoop-search.ps1
index d94b18660d..0698c7a441 100644
--- a/libexec/scoop-search.ps1
+++ b/libexec/scoop-search.ps1
@@ -140,8 +140,7 @@ function search_remotes($query) {
} | Where-Object { $_.results }
if ($results.count -gt 0) {
- Write-Host "Results from other known buckets...
-(add them using 'scoop bucket add ')"
+ Write-Host "Results from other known buckets...`n(add them using 'scoop bucket add ')"
}
$remote_list = @()
@@ -159,7 +158,7 @@ function search_remotes($query) {
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
- Select-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
+ Find-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
Select-Object -Property name, version, bucket, binary |
ForEach-Object {
$list.Add([PSCustomObject]@{
diff --git a/libexec/scoop-shim.ps1 b/libexec/scoop-shim.ps1
index 877b65b2e9..35314e069c 100644
--- a/libexec/scoop-shim.ps1
+++ b/libexec/scoop-shim.ps1
@@ -4,7 +4,7 @@
#
# To add a custom shim, use the 'add' subcommand:
#
-# scoop shim add [...]
+# scoop shim add [[--|--%] ...]
#
# To remove shims, use the 'rm' subcommand: (CAUTION: this could remove shims added by an app manifest)
#
@@ -25,11 +25,15 @@
# Options:
# -g, --global Manipulate global shim(s)
#
-# HINT: The FIRST double-hyphen '--', if any, will be treated as the POSIX-style command option terminator
-# and will NOT be included in arguments, so if you want to pass arguments like '-g' or '--global' to
-# the shim, put them after a '--'. Note that in PowerShell, you must use a QUOTED '--', e.g.,
-#
-# scoop shim add myapp 'D:\path\myapp.exe' '--' myapp_args --global
+# HINT: The FIRST terminator token ('--' or PowerShell '--%'), if any, is treated as the option
+# terminator and will NOT be included; everything after it is passed to the shim.
+# So if you want to pass arguments like '-g' or '--global' to the shim, put them after a '--' or '--%'.
+# Examples:
+# POSIX-style: scoop shim add myapp 'D:\path\myapp.exe' '--' myapp_args --global
+# PowerShell: scoop shim add myapp D:\path\myapp.exe --% myapp_args --global
+# Notes:
+# - In PowerShell, '--' should be quoted to avoid parsing.
+# - '--%' disables PowerShell parsing of the remainder; pass literals as needed.
param($SubCommand)
@@ -48,7 +52,7 @@ if ($SubCommand -notin @('add', 'rm', 'list', 'info', 'alter')) {
}
$opt, $other, $err = getopt $Args 'g' 'global'
-if ($err) { "scoop shim: $err"; exit 1 }
+if ($err) { error "scoop shim: $err"; exit 1 }
$global = $opt.g -or $opt.global
@@ -112,13 +116,10 @@ switch ($SubCommand) {
}
}
if ($commandPath -and (Test-Path $commandPath)) {
- Write-Host "Adding $(if ($global) { 'global' } else { 'local' }) shim " -NoNewline
- Write-Host $shimName -ForegroundColor Cyan -NoNewline
- Write-Host '...'
+ Write-Host "Adding $(if ($global) { 'global' } else { 'local' }) shim $([char]0x1b)[36m$shimName$([char]0x1b)[0m..."
shim $commandPath $global $shimName $commandArgs
} else {
- Write-Host "ERROR: Command path does not exist: " -ForegroundColor Red -NoNewline
- Write-Host $($other[1]) -ForegroundColor Cyan
+ error "Command path does not exist: $([char]0x1b)[36m$($other[1])$([char]0x1b)[31m"
exit 3
}
}
@@ -133,8 +134,7 @@ switch ($SubCommand) {
}
if ($failed) {
$failed | ForEach-Object {
- Write-Host "ERROR: $(if ($global) { 'Global' } else {'Local' }) shim not found: " -ForegroundColor Red -NoNewline
- Write-Host $_ -ForegroundColor Cyan
+ error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$_$([char]0x1b)[31m"
}
exit 3
}
@@ -147,8 +147,7 @@ switch ($SubCommand) {
$pattern = $_
[void][Regex]::New($pattern)
} catch {
- Write-Host "ERROR: Invalid pattern: " -ForegroundColor Red -NoNewline
- Write-Host $pattern -ForegroundColor Magenta
+ error "Invalid pattern: $([char]0x1b)[35m$pattern$([char]0x1b)[31m"
exit 1
}
}
@@ -172,11 +171,9 @@ switch ($SubCommand) {
if ($shimPath) {
Get-ShimInfo $shimPath
} else {
- Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline
- Write-Host $shimName -ForegroundColor Cyan
+ error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$shimName$([char]0x1b)[31m"
if (Get-ShimPath $shimName (!$global)) {
- Write-Host "But a $(if ($global) { 'local' } else {'global' }) shim exists, " -NoNewline
- Write-Host "run 'scoop shim info $shimName$(if (!$global) { ' --global' })' to show its info"
+ Write-Host "But a $(if ($global) { 'local' } else { 'global' }) shim exists, run 'scoop shim info $shimName$(if (!$global) { ' --global' })' to show its info."
exit 2
}
exit 3
@@ -188,9 +185,7 @@ switch ($SubCommand) {
if ($shimPath) {
$shimInfo = Get-ShimInfo $shimPath
if ($null -eq $shimInfo.Alternatives) {
- Write-Host 'ERROR: No alternatives of ' -ForegroundColor Red -NoNewline
- Write-Host $shimName -ForegroundColor Cyan -NoNewline
- Write-Host ' found.' -ForegroundColor Red
+ error "No alternatives of $([char]0x1b)[36m$shimName$([char]0x1b)[31m found."
exit 2
}
$shimInfo.Alternatives = $shimInfo.Alternatives.Split(' ')
@@ -199,18 +194,10 @@ switch ($SubCommand) {
}
$selected = $Host.UI.PromptForChoice("Alternatives of '$shimName' command", "Please choose one that provides '$shimName' as default:", $altApps, 0)
if ($selected -eq 0) {
- Write-Host 'INFO: ' -ForegroundColor Blue -NoNewline
- Write-Host $shimName -ForegroundColor Cyan -NoNewline
- Write-Host ' is already from ' -NoNewline
- Write-Host $shimInfo.Source -ForegroundColor DarkYellow -NoNewline
- Write-Host ', nothing changed.'
+ Write-Host "$([char]0x1b)[36m$shimName$([char]0x1b)[0m is already from $([char]0x1b)[33m$($shimInfo.Source)$([char]0x1b)[0m, nothing changed."
} else {
$newApp = $shimInfo.Alternatives[$selected]
- Write-Host 'Use ' -NoNewline
- Write-Host $shimName -ForegroundColor Cyan -NoNewline
- Write-Host ' from ' -NoNewline
- Write-Host $newApp -ForegroundColor DarkYellow -NoNewline
- Write-Host ' as default...' -NoNewline
+ Write-Host "Use $([char]0x1b)[36m$shimName$([char]0x1b)[0m from $([char]0x1b)[33m$newApp$([char]0x1b)[0m as default... " -NoNewline
$pathNoExt = strip_ext $shimPath
'', '.shim', '.cmd', '.ps1' | ForEach-Object {
$oldShimPath = "$pathNoExt$_"
@@ -222,14 +209,12 @@ switch ($SubCommand) {
}
}
}
- Write-Host 'done.'
+ Write-Host 'Done.'
}
} else {
- Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline
- Write-Host $shimName -ForegroundColor Cyan
+ error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$shimName$([char]0x1b)[31m"
if (Get-ShimPath $shimName (!$global)) {
- Write-Host "But a $(if ($global) { 'local' } else {'global' }) shim exists, " -NoNewline
- Write-Host "run 'scoop shim alter $shimName$(if (!$global) { ' --global' })' to alternate its source"
+ Write-Host "But a $(if ($global) { 'local' } else { 'global' }) shim exists, run 'scoop shim alter $shimName$(if (!$global) { ' --global' })' to alternate its source."
exit 2
}
exit 3
diff --git a/libexec/scoop-unhold.ps1 b/libexec/scoop-unhold.ps1
index 4e2413a340..88a859c81c 100644
--- a/libexec/scoop-unhold.ps1
+++ b/libexec/scoop-unhold.ps1
@@ -15,7 +15,7 @@
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
$opt, $apps, $err = getopt $args 'g' 'global'
-if ($err) { "scoop unhold: $err"; exit 1 }
+if ($err) { error "scoop unhold: $err"; exit 1 }
$global = $opt.g -or $opt.global
diff --git a/libexec/scoop-uninstall.ps1 b/libexec/scoop-uninstall.ps1
index 5bdd57e5d1..7beb0d260e 100644
--- a/libexec/scoop-uninstall.ps1
+++ b/libexec/scoop-uninstall.ps1
@@ -10,6 +10,7 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\system.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
+. "$PSScriptRoot\..\lib\download.ps1" # url_filename
. "$PSScriptRoot\..\lib\shortcuts.ps1"
. "$PSScriptRoot\..\lib\psmodules.ps1"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
@@ -58,6 +59,7 @@ if (!$apps) { exit 0 }
$manifest = installed_manifest $app $version $global
$install = install_info $app $version $global
$architecture = $install.architecture
+ $bucket = $install.bucket
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture
@@ -74,7 +76,7 @@ if (!$apps) { exit 0 }
continue
}
- Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global $global -Uninstall
+ Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall
rm_shims $app $manifest $global $architecture
rm_startmenu_shortcuts $manifest $global $architecture
diff --git a/libexec/scoop-update.ps1 b/libexec/scoop-update.ps1
index 9d41906eca..bc590a13f6 100644
--- a/libexec/scoop-update.ps1
+++ b/libexec/scoop-update.ps1
@@ -30,7 +30,7 @@ if (get_config USE_SQLITE_CACHE) {
}
$opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip-hash-check', 'quiet', 'all'
-if ($err) { "scoop update: $err"; exit 1 }
+if ($err) { error "scoop update: $err"; exit 1 }
$global = $opt.g -or $opt.global
$force = $opt.f -or $opt.force
$check_hash = !($opt.s -or $opt.'skip-hash-check')
@@ -136,7 +136,7 @@ function Sync-Scoop {
# reset branch HEAD
Invoke-Git -Path $currentdir -ArgumentList @('reset', '--hard', "origin/$configBranch", '-q')
} else {
- Invoke-Git -Path $currentdir -ArgumentList @('pull', '-q')
+ Invoke-Git -Path $currentdir -ArgumentList @('pull', '--tags', '--force', '-q')
}
$res = $lastexitcode
@@ -341,7 +341,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture
Write-Host "Uninstalling '$app' ($old_version)"
- Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Uninstall
+ Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Global:$global -Uninstall
rm_shims $app $old_manifest $global $architecture
# If a junction was used during install, that will have been used
@@ -400,7 +400,7 @@ if (-not ($apps -or $all)) {
success 'Scoop was updated successfully!'
} else {
if ($global -and !(is_admin)) {
- 'ERROR: You need admin rights to update global apps.'; exit 1
+ error 'You need admin rights to update global apps.'; exit 1
}
$outdated = @()
diff --git a/libexec/scoop-virustotal.ps1 b/libexec/scoop-virustotal.ps1
index 8fe63359cf..d56eb6e8b4 100644
--- a/libexec/scoop-virustotal.ps1
+++ b/libexec/scoop-virustotal.ps1
@@ -36,7 +36,7 @@
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
-if ($err) { "scoop virustotal: $err"; exit 1 }
+if ($err) { error "scoop virustotal: $err"; exit 1 }
$all = $apps -eq '*' -or $opt.a -or $opt.all
if (!$apps -and !$all) { my_usage; exit 1 }
$architecture = Get-DefaultArchitecture
diff --git a/schema.json b/schema.json
index 6c24d4da20..9457c391e7 100644
--- a/schema.json
+++ b/schema.json
@@ -50,6 +50,7 @@
},
"mode": {
"enum": [
+ "github",
"download",
"extract",
"json",
diff --git a/test/Scoop-Core.Tests.ps1 b/test/Scoop-Core.Tests.ps1
index d1cc9defe3..a857ec47e4 100644
--- a/test/Scoop-Core.Tests.ps1
+++ b/test/Scoop-Core.Tests.ps1
@@ -393,3 +393,17 @@ Describe 'Format Architecture String' -Tag 'Scoop' {
{ Format-ArchitectureString 'PPC' } | Should -Throw "Invalid architecture: 'ppc'"
}
}
+
+Describe 'substitute' -Tag 'Scoop' {
+ It 'should properly handle keys that are substrings of other keys' {
+ $params = @{}
+
+ # Run repeatedly (10 times) to reduce the likelihood of accidental correct ordering.
+ 1 .. 10 | ForEach-Object {
+ $params["`$name$($_)"] = "$($_).exe"
+ $params["`$name$($_)NoExt"] = "$_"
+
+ substitute ("`$name$($_)NoExt") $params $false | Should -Be "$_"
+ }
+ }
+}
diff --git a/test/Scoop-Database.Tests.ps1 b/test/Scoop-Database.Tests.ps1
new file mode 100644
index 0000000000..354b7b1118
--- /dev/null
+++ b/test/Scoop-Database.Tests.ps1
@@ -0,0 +1,57 @@
+Describe 'database version selection' -Tag 'Scoop' {
+ BeforeAll {
+ . "$PSScriptRoot\Scoop-TestLib.ps1"
+ . "$PSScriptRoot\..\lib\core.ps1"
+ . "$PSScriptRoot\..\lib\versions.ps1"
+ . "$PSScriptRoot\..\lib\database.ps1"
+ }
+
+ It 'chooses the semantically latest row within one result set' {
+ $rows = @(
+ [pscustomobject]@{ version = '1.0.7' }
+ [pscustomobject]@{ version = '1.0.31' }
+ )
+
+ (Get-LatestScoopDBRow -Rows $rows).version | Should -Be '1.0.31'
+ }
+
+ It 'returns null when no rows are provided' {
+ Get-LatestScoopDBRow -Rows @() | Should -Be $null
+ }
+
+ It 'returns the latest semantic version per name and bucket' {
+ $result = New-Object System.Data.DataTable
+ [void]$result.Columns.Add('name', [string])
+ [void]$result.Columns.Add('version', [string])
+ [void]$result.Columns.Add('bucket', [string])
+ [void]$result.Columns.Add('binary', [string])
+ [void]$result.Rows.Add('copilot-cli', '1.0.7', 'main', 'copilot')
+ [void]$result.Rows.Add('copilot-cli', '1.0.31', 'main', 'copilot')
+ [void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
+ [void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')
+ [void]$result.Rows.Add('zotero', '7.0.9', 'he0119', 'zotero')
+ [void]$result.Rows.Add('zotero', '7.0.20', 'he0119', 'zotero')
+
+ $latest = @(Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket'))
+
+ $latest.Count | Should -Be 3
+ (@($latest | Where-Object { $_.name -eq 'copilot-cli' -and $_.bucket -eq 'main' })[0]).version | Should -Be '1.0.31'
+ (@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'extras' })[0]).version | Should -Be '7.0.20'
+ (@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'he0119' })[0]).version | Should -Be '7.0.20'
+ }
+
+ It 'returns the latest semantic version when no grouping is requested' {
+ $result = New-Object System.Data.DataTable
+ [void]$result.Columns.Add('name', [string])
+ [void]$result.Columns.Add('version', [string])
+ [void]$result.Columns.Add('bucket', [string])
+ [void]$result.Columns.Add('binary', [string])
+ [void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
+ [void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')
+
+ $latest = @(Select-LatestScoopDBRow -Table $result)
+
+ $latest.Count | Should -Be 1
+ $latest[0].version | Should -Be '7.0.20'
+ }
+}
diff --git a/test/Scoop-GetOpts.Tests.ps1 b/test/Scoop-GetOpts.Tests.ps1
index c55781ecbf..5608cb73ca 100644
--- a/test/Scoop-GetOpts.Tests.ps1
+++ b/test/Scoop-GetOpts.Tests.ps1
@@ -81,13 +81,32 @@ Describe 'getopt' -Tag 'Scoop' {
$opt, $rem, $err = getopt '--long-arg', '--' '' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.'long-arg' | Should -BeTrue
- $rem[0] | Should -BeNullOrEmpty
+ $rem | Should -BeNullOrEmpty
+ }
+
+ It 'handles remainder args after the option terminator' {
$opt, $rem, $err = getopt '--long-arg', '--', '-x', '-y' 'xy' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.'long-arg' | Should -BeTrue
- $opt.'x' | Should -BeNullOrEmpty
- $opt.'y' | Should -BeNullOrEmpty
+ $opt.Keys | Should -Not -Contain 'x'
+ $opt.Keys | Should -Not -Contain 'y'
$rem[0] | Should -Be '-x'
$rem[1] | Should -Be '-y'
}
+
+ It 'handles PowerShell stop-parsing token' {
+ $opt, $rem, $err = getopt '--long-arg', '--%' '' 'long-arg'
+ $err | Should -BeNullOrEmpty
+ $opt.'long-arg' | Should -BeTrue
+ $rem | Should -BeNullOrEmpty
+ }
+
+ It 'handles remainder args after PowerShell stop-parsing token' {
+ $opt, $rem, $err = getopt @('--long-arg', '--%', '--from', 'there', '--to', 'here') '' 'long-arg'
+ $err | Should -BeNullOrEmpty
+ $opt.Keys | Should -Not -Contain 'from'
+ $opt.Keys | Should -Not -Contain 'to'
+ $rem | Should -Be @('--from', 'there', '--to', 'here')
+ }
+
}