diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 2ab39dcf2c..eec84250f6 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -31,9 +31,9 @@ labels: "bug" ### System details -**Windows version:** [e.g. 7, 8, 10] +**Windows version:** [e.g. 7, 8, 10, 11] -**OS architecture:** [e.g. 32bit, 64bit] +**OS architecture:** [e.g. 32bit, 64bit, arm64] **PowerShell version:** [output of `"$($PSVersionTable.PSVersion)"`] diff --git a/.gitignore b/.gitignore index cde3143c76..fdc96d2a52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.log .DS_Store ._.DS_Store scoop.sublime-workspace diff --git a/.vscode/settings.json b/.vscode/settings.json index a848b1f5b4..b250385bac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,9 @@ "powershell.codeFormatting.preset": "OTBS", "powershell.codeFormatting.alignPropertyValuePairs": true, "powershell.codeFormatting.ignoreOneLineBlock": true, + "powershell.codeFormatting.useConstantStrings": true, + "powershell.codeFormatting.useCorrectCasing": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, "files.exclude": { "**/.git": true, "**/.svn": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e0bb8791..94beb9b878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## [v0.3.0](https://github.com/ScoopInstaller/Scoop/compare/v0.2.4...v0.3.0) - 2022-10-10 + +### Features + +- **install:** Add support for ARM64 architecture ([#5154](https://github.com/ScoopInstaller/Scoop/issues/5154)) +- **install:** Show the running process ([#5102](https://github.com/ScoopInstaller/Scoop/issues/5102)) +- **getopt:** Support option terminator (`--`) ([#5121](https://github.com/ScoopInstaller/Scoop/issues/5121)) +- **subdir:** Allow subdir in 'bucket' ([#5119](https://github.com/ScoopInstaller/Scoop/issues/5119)) +- **scoop-config:** Allow 'hold_update_until' be set manually ([#5100](https://github.com/ScoopInstaller/Scoop/issues/5100)) +- **scoop-(un)hold:** Support `scoop (un)hold scoop` ([#5089](https://github.com/ScoopInstaller/Scoop/issues/5089)) +- **scoop-update:** Stash uncommitted changes before update ([#5091](https://github.com/ScoopInstaller/Scoop/issues/5091)) + +### Bug Fixes + +- **config:** Change config option to snake_case in file and SCREAMING_CASE in code ([#5116](https://github.com/ScoopInstaller/Scoop/issues/5116)) +- **jsonpath:** Prevent converting date string to DateTime in JSONPath ([#5130](https://github.com/ScoopInstaller/Scoop/issues/5130)) +- **psmodule:** Remove folder recursively when unlinking previous module path ([#5127](https://github.com/ScoopInstaller/Scoop/issues/5127)) +- **scoop-update:** Add `uninstall_psmodule` to update process ([#5136](https://github.com/ScoopInstaller/Scoop/issues/5136)) + +### Code Refactoring + +- **download:** Rename `dl()` to `Invoke-Download()` ([#5143](https://github.com/ScoopInstaller/Scoop/issues/5143)) +- **path:** Use 'Convert-Path()' instead of 'Resolve-Path()' ([#5109](https://github.com/ScoopInstaller/Scoop/issues/5109)) +- **scoop-shim:** Use `getopt` to parse arguments ([#5125](https://github.com/ScoopInstaller/Scoop/issues/5125)) + +### Builds + +- **checkver:** Implement SourceForge checkver functionality ([#5113](https://github.com/ScoopInstaller/Scoop/issues/5113), [#5163](https://github.com/ScoopInstaller/Scoop/issues/5163)) +- **checkurls:** Allow checking URLs from private_hosts ([#5152](https://github.com/ScoopInstaller/Scoop/issues/5152)) +- **schema:** Set manifest schema to be stricter ([#5093](https://github.com/ScoopInstaller/Scoop/issues/5093)) +- **vscode:** Tweak VSCode setting ([#5149](https://github.com/ScoopInstaller/Scoop/issues/5149)) + ## [v0.2.4](https://github.com/ScoopInstaller/Scoop/compare/v0.2.3...v0.2.4) - 2022-08-08 ### Features diff --git a/bin/auto-pr.ps1 b/bin/auto-pr.ps1 index b44f760c4d..390d2117ec 100644 --- a/bin/auto-pr.ps1 +++ b/bin/auto-pr.ps1 @@ -66,7 +66,7 @@ param( if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) { $Dir = Split-Path $App } elseif ($Dir) { - $Dir = Resolve-Path $Dir + $Dir = Convert-Path $Dir } else { throw "'-Dir' parameter required if '-App' is not a filepath!" } diff --git a/bin/checkhashes.ps1 b/bin/checkhashes.ps1 index a6c93e6721..fac1787839 100644 --- a/bin/checkhashes.ps1 +++ b/bin/checkhashes.ps1 @@ -49,7 +49,7 @@ param( . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\unix.ps1" -$Dir = Resolve-Path $Dir +$Dir = Convert-Path $Dir if ($ForceUpdate) { $Update = $true } # Cleanup if (!$UseCache) { Remove-Item "$cachedir\*HASH_CHECK*" -Force } @@ -60,9 +60,10 @@ function err ([String] $name, [String[]] $message) { } $MANIFESTS = @() -foreach ($single in Get-ChildItem $Dir "$App.json") { - $name = (strip_ext $single.Name) - $manifest = parse_json "$Dir\$($single.Name)" +foreach ($single in Get-ChildItem $Dir -Filter "$App.json" -Recurse) { + $name = $single.BaseName + $file = $single.FullName + $manifest = parse_json $file # Skip nighly manifests, since their hash validation is skipped if ($manifest.version -eq 'nightly') { continue } @@ -79,6 +80,8 @@ foreach ($single in Get-ChildItem $Dir "$App.json") { hash $manifest '64bit' | ForEach-Object { $hashes += $_ } script:url $manifest '32bit' | ForEach-Object { $urls += $_ } hash $manifest '32bit' | ForEach-Object { $hashes += $_ } + script:url $manifest 'arm64' | ForEach-Object { $urls += $_ } + hash $manifest 'arm64' | ForEach-Object { $hashes += $_ } } else { err $name 'Manifest does not contain URL property.' continue @@ -92,6 +95,7 @@ foreach ($single in Get-ChildItem $Dir "$App.json") { $MANIFESTS += @{ app = $name + file = $file manifest = $manifest urls = $urls hashes = $hashes @@ -113,7 +117,7 @@ foreach ($current in $MANIFESTS) { $version = 'HASH_CHECK' $tmp = $expected_hash -split ':' - dl_with_cache $current.app $version $_ $null $null -use_cache:$UseCache + Invoke-CachedDownload $current.app $version $_ $null $null -use_cache:$UseCache $to_check = fullpath (cache_path $current.app $version $_) $actual_hash = compute_hash $to_check $algorithm @@ -158,23 +162,27 @@ foreach ($current in $MANIFESTS) { # Defaults to zero, don't know, which architecture is available $64bit_count = 0 $32bit_count = 0 + $arm64_count = 0 + # 64bit is get, donwloaded and added first if ($platforms.Contains('64bit')) { $64bit_count = $current.manifest.architecture.'64bit'.hash.Count - # 64bit is get, donwloaded and added first $current.manifest.architecture.'64bit'.hash = $actuals[0..($64bit_count - 1)] } if ($platforms.Contains('32bit')) { $32bit_count = $current.manifest.architecture.'32bit'.hash.Count - $max = $64bit_count + $32bit_count - 1 # Edge case if manifest contains 64bit and 32bit. - $current.manifest.architecture.'32bit'.hash = $actuals[($64bit_count)..$max] + $current.manifest.architecture.'32bit'.hash = $actuals[($64bit_count)..($64bit_count + $32bit_count - 1)] + } + if ($platforms.Contains('arm64')) { + $arm64_count = $current.manifest.architecture.'arm64'.hash.Count + $current.manifest.architecture.'arm64'.hash = $actuals[($64bit_count + $32bit_count)..($64bit_count + $32bit_count + $arm64_count - 1)] } } Write-Host "Writing updated $($current.app) manifest" -ForegroundColor DarkGreen $current.manifest = $current.manifest | ConvertToPrettyJson - $path = Resolve-Path "$Dir\$($current.app).json" + $path = Convert-Path $current.file [System.IO.File]::WriteAllLines($path, $current.manifest) } } diff --git a/bin/checkurls.ps1 b/bin/checkurls.ps1 index 733858e875..64ab0c1840 100644 --- a/bin/checkurls.ps1 +++ b/bin/checkurls.ps1 @@ -30,12 +30,12 @@ param( . "$PSScriptRoot\..\lib\manifest.ps1" . "$PSScriptRoot\..\lib\install.ps1" -$Dir = Resolve-Path $Dir +$Dir = Convert-Path $Dir $Queue = @() -Get-ChildItem $Dir "$App.json" | ForEach-Object { - $manifest = parse_json "$Dir\$($_.Name)" - $Queue += , @($_.Name, $manifest) +Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { + $manifest = parse_json $_.FullName + $Queue += , @($_.BaseName, $manifest) } Write-Host '[' -NoNewLine @@ -62,6 +62,13 @@ function test_dl([String] $url, $cookies) { $wreq.Headers.Add('Cookie', (cookie_header $cookies)) } } + + get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object { + (ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object { + $wreq.Headers[$_.Key] = $_.Value + } + } + $wres = $null try { $wres = $wreq.GetResponse() @@ -91,6 +98,7 @@ foreach ($man in $Queue) { } else { script:url $manifest '64bit' | ForEach-Object { $urls += $_ } script:url $manifest '32bit' | ForEach-Object { $urls += $_ } + script:url $manifest 'arm64' | ForEach-Object { $urls += $_ } } $urls | ForEach-Object { @@ -125,7 +133,7 @@ foreach ($man in $Queue) { Write-Host $failed -NoNewLine -ForegroundColor Red } Write-Host '] ' -NoNewLine - Write-Host (strip_ext $name) + Write-Host $name $errors | ForEach-Object { Write-Host " > $_" -ForegroundColor DarkRed diff --git a/bin/checkver.ps1 b/bin/checkver.ps1 index e87ad98ea5..0205a36639 100644 --- a/bin/checkver.ps1 +++ b/bin/checkver.ps1 @@ -78,10 +78,10 @@ param( if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) { $Dir = Split-Path $App - $files = Get-ChildItem $Dir (Split-Path $App -Leaf) + $files = Get-ChildItem $Dir -Filter (Split-Path $App -Leaf) } elseif ($Dir) { - $Dir = Resolve-Path $Dir - $files = Get-ChildItem $Dir "$App.json" + $Dir = Convert-Path $Dir + $files = Get-ChildItem $Dir -Filter "$App.json" -Recurse } else { throw "'-Dir' parameter required if '-App' is not a filepath!" } @@ -97,9 +97,10 @@ if ($App -eq '*' -and $Version -ne '') { $Queue = @() $json = '' $files | ForEach-Object { - $json = parse_json "$Dir\$($_.Name)" + $file = $_.FullName + $json = parse_json $file if ($json.checkver) { - $Queue += , @($_.Name, $json) + $Queue += , @($_.BaseName, $json, $file) } } @@ -109,7 +110,7 @@ Get-EventSubscriber | Unregister-Event # start all downloads $Queue | ForEach-Object { - $name, $json = $_ + $name, $json, $file = $_ $substitutions = Get-VersionSubstitution $json.version # 'autoupdate.ps1' @@ -121,18 +122,32 @@ $Queue | ForEach-Object { } Register-ObjectEvent $wc downloadDataCompleted -ErrorAction Stop | Out-Null - $githubRegex = '\/releases\/tag\/(?:v|V)?([\d.]+)' - - $url = $json.homepage + # Not Specified 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 = '' } - $regex = '' + $jsonpath = '' $xpath = '' $replace = '' $useGithubAPI = $false + # 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" @@ -148,11 +163,38 @@ $Queue | ForEach-Object { if ($json.checkver.PSObject.Properties.Count -eq 1) { $useGithubAPI = $true } } - if ($json.checkver.re) { - $regex = $json.checkver.re + # SourceForge + if ($regex) { + $sourceforgeRegex = $regex + } else { + $sourceforgeRegex = '(?!\.)([\d.]+)(?<=\d)' } - if ($json.checkver.regex) { - $regex = $json.checkver.regex + if ($json.checkver -eq 'sourceforge') { + if ($json.homepage -match '//(sourceforge|sf)\.net/projects/(?[^/]+)(/files/(?[^/]+))?|//(?[^.]+)\.(sourceforge\.(net|io)|sf\.net)') { + $project = $Matches['project'] + $path = $Matches['path'] + } else { + $project = strip_ext $name + } + $url = "https://sourceforge.net/projects/$project/rss" + if ($path) { + $url = $url + '?path=/' + $path.TrimStart('/') + } + $regex = "CDATA\[/$path/.*?$sourceforgeRegex.*?\]".Replace('//', '/') + } + if ($json.checkver.sourceforge) { + if ($json.checkver.sourceforge -is [System.String] -and $json.checkver.sourceforge -match '(?[\w-]*)(/(?.*))?') { + $project = $Matches['project'] + $path = $Matches['path'] + } else { + $project = $json.checkver.sourceforge.project + $path = $json.checkver.sourceforge.path + } + $url = "https://sourceforge.net/projects/$project/rss" + if ($path) { + $url = $url + '?path=/' + $path.TrimStart('/') + } + $regex = "CDATA\[/$path/.*?$sourceforgeRegex.*?\]".Replace('//', '/') } if ($json.checkver.jp) { @@ -165,7 +207,7 @@ $Queue | ForEach-Object { $xpath = $json.checkver.xpath } - if ($json.checkver.replace -and $json.checkver.replace.GetType() -eq [System.String]) { + if ($json.checkver.replace -is [System.String]) { # If `checkver` is [System.String], it has a method called `Replace` $replace = $json.checkver.replace } @@ -185,7 +227,8 @@ $Queue | ForEach-Object { $url = substitute $url $substitutions $state = New-Object psobject @{ - app = (strip_ext $name); + app = $name; + file = $file; url = $url; regex = $regex; json = $json; @@ -213,6 +256,7 @@ while ($in_progress -gt 0) { $state = $ev.SourceEventArgs.UserState $app = $state.app + $file = $state.file $json = $state.json $url = $state.url $regexp = $state.regex @@ -319,7 +363,7 @@ while ($in_progress -gt 0) { # Skip actual only if versions are same and there is no -f if (($ver -eq $expected_ver) -and !$ForceUpdate -and $SkipUpdated) { continue } - Write-Host "$App`: " -NoNewline + Write-Host "$app`: " -NoNewline # version hasn't changed (step over if forced update) if ($ver -eq $expected_ver -and !$ForceUpdate) { @@ -345,7 +389,7 @@ while ($in_progress -gt 0) { Write-Host 'Forcing autoupdate!' -ForegroundColor DarkMagenta } try { - Invoke-AutoUpdate $App $Dir $json $ver $matchesHashtable # 'autoupdate.ps1' + Invoke-AutoUpdate $app $file $json $ver $matchesHashtable # 'autoupdate.ps1' } catch { if ($ThrowError) { throw $_ diff --git a/bin/describe.ps1 b/bin/describe.ps1 index ae573ba484..f9e024e4c4 100644 --- a/bin/describe.ps1 +++ b/bin/describe.ps1 @@ -24,12 +24,12 @@ param( . "$PSScriptRoot\..\lib\manifest.ps1" . "$PSScriptRoot\..\lib\description.ps1" -$Dir = Resolve-Path $Dir +$Dir = Convert-Path $Dir $Queue = @() -Get-ChildItem $Dir "$App.json" | ForEach-Object { - $manifest = parse_json "$Dir\$($_.Name)" - $Queue += , @(($_.Name -replace '\.json$', ''), $manifest) +Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { + $manifest = parse_json $_.FullName + $Queue += , @($_.BaseName, $manifest) } $Queue | ForEach-Object { diff --git a/bin/formatjson.ps1 b/bin/formatjson.ps1 index a9f67cc447..e66c6f69a3 100644 --- a/bin/formatjson.ps1 +++ b/bin/formatjson.ps1 @@ -31,15 +31,14 @@ param( . "$PSScriptRoot\..\lib\manifest.ps1" . "$PSScriptRoot\..\lib\json.ps1" -$Dir = Resolve-Path $Dir - -Get-ChildItem $Dir "$App.json" | ForEach-Object { - if ($PSVersionTable.PSVersion.Major -gt 5) { $_ = $_.Name } # Fix for pwsh +$Dir = Convert-Path $Dir +Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { + $file = $_.FullName # beautify - $json = parse_json "$Dir\$_" | ConvertToPrettyJson + $json = parse_json $file | ConvertToPrettyJson # convert to 4 spaces $json = $json -replace "`t", ' ' - [System.IO.File]::WriteAllLines("$Dir\$_", $json) + [System.IO.File]::WriteAllLines($file, $json) } diff --git a/bin/install.ps1 b/bin/install.ps1 index 68dcf6671c..b3942513e4 100644 --- a/bin/install.ps1 +++ b/bin/install.ps1 @@ -41,10 +41,10 @@ if (Get-Command -Name 'scoop' -ErrorAction SilentlyContinue) { $dir = ensure (versiondir 'scoop' 'current') # download scoop zip -$zipurl = 'https://github.com/ScoopInstaller/Scoop/archive/master.zip' -$zipfile = "$dir\scoop.zip" +$zipUrl = 'https://github.com/ScoopInstaller/Scoop/archive/master.zip' +$zipFile = "$dir\scoop.zip" Write-Output 'Downloading scoop...' -dl $zipurl $zipfile +Invoke-WebRequest -Uri $zipUrl -OutFile $zipFile Write-Output 'Extracting...' Add-Type -Assembly "System.IO.Compression.FileSystem" @@ -57,11 +57,11 @@ shim "$dir\bin\scoop.ps1" $false # download main bucket $dir = "$scoopdir\buckets\main" -$zipurl = 'https://github.com/ScoopInstaller/Main/archive/master.zip' -$zipfile = "$dir\main-bucket.zip" +$zipUrl = 'https://github.com/ScoopInstaller/Main/archive/master.zip' +$zipFile = "$dir\main-bucket.zip" Write-Output 'Downloading main bucket...' -New-Item $dir -Type Directory -Force | Out-Null -dl $zipurl $zipfile +New-Item -Path $dir -Type Directory -Force | Out-Null +Invoke-WebRequest -Uri $zipUrl -OutFile $zipFile Write-Output 'Extracting...' [IO.Compression.ZipFile]::ExtractToDirectory($zipfile, "$dir\_tmp") @@ -70,7 +70,7 @@ Remove-Item "$dir\_tmp", $zipfile -Recurse -Force ensure_robocopy_in_path -scoop config lastupdate ([System.DateTime]::Now.ToString('o')) +set_config LAST_UPDATE ([System.DateTime]::Now.ToString('o')) | Out-Null success 'Scoop was installed successfully!' Write-Output "Type 'scoop help' for instructions." diff --git a/bin/missing-checkver.ps1 b/bin/missing-checkver.ps1 index abbc1cc9b1..3ba2d2d660 100644 --- a/bin/missing-checkver.ps1 +++ b/bin/missing-checkver.ps1 @@ -26,7 +26,7 @@ param( . "$PSScriptRoot\..\lib\core.ps1" . "$PSScriptRoot\..\lib\manifest.ps1" -$Dir = Resolve-Path $Dir +$Dir = Convert-Path $Dir Write-Host '[' -NoNewLine Write-Host 'C' -NoNewLine -ForegroundColor Green @@ -36,8 +36,8 @@ Write-Host 'A' -NoNewLine -ForegroundColor Cyan Write-Host ']utoupdate' Write-Host ' | |' -Get-ChildItem $Dir "$App.json" | ForEach-Object { - $json = parse_json "$Dir\$($_.Name)" +Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { + $json = parse_json $_.FullName if ($SkipSupported -and $json.checkver -and $json.autoupdate) { return } @@ -48,5 +48,5 @@ Get-ChildItem $Dir "$App.json" | ForEach-Object { Write-Host '[' -NoNewLine Write-Host $(if ($json.autoupdate) { 'A' } else { ' ' }) -NoNewLine -ForegroundColor Cyan Write-Host '] ' -NoNewLine - Write-Host (strip_ext $_.Name) + Write-Host $_.BaseName } diff --git a/lib/autoupdate.ps1 b/lib/autoupdate.ps1 index 7cf4f62053..dc9dbffeb3 100644 --- a/lib/autoupdate.ps1 +++ b/lib/autoupdate.ps1 @@ -267,7 +267,7 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u write-host -f Green $basename -NoNewline write-host -f DarkYellow ' to compute hashes!' try { - dl_with_cache $app $version $url $null $null $true + 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" @@ -449,7 +449,7 @@ function Invoke-AutoUpdate { # 'Set-Content -Encoding ASCII' don't works in PowerShell 5 # Wait for 'UTF8NoBOM' Encoding in PowerShell 7 # $Manifest | ConvertToPrettyJson | Set-Content -Path (Join-Path $Path "$AppName.json") -Encoding UTF8NoBOM - [System.IO.File]::WriteAllLines((Join-Path $Path "$AppName.json"), (ConvertToPrettyJson $Manifest)) + [System.IO.File]::WriteAllLines($Path, (ConvertToPrettyJson $Manifest)) # notes $note = "`nUpdating note:" if ($Manifest.autoupdate.note) { @@ -457,7 +457,7 @@ function Invoke-AutoUpdate { $hasNote = $true } if ($Manifest.autoupdate.architecture) { - '64bit', '32bit' | ForEach-Object { + '64bit', '32bit', 'arm64' | ForEach-Object { if ($Manifest.autoupdate.architecture.$_.note) { $note += "`n$_-arch: $($Manifest.autoupdate.architecture.$_.note)" $hasNote = $true diff --git a/lib/buckets.ps1 b/lib/buckets.ps1 index c22c291220..2a03561210 100644 --- a/lib/buckets.ps1 +++ b/lib/buckets.ps1 @@ -50,7 +50,7 @@ function known_buckets { } function apps_in_bucket($dir) { - return Get-ChildItem $dir | Where-Object { $_.Name.EndsWith('.json') } | ForEach-Object { $_.Name -replace '.json$', '' } + return (Get-ChildItem $dir -Filter '*.json' -Recurse).BaseName } function Get-LocalBucket { diff --git a/lib/core.ps1 b/lib/core.ps1 index 94f555587d..db305b5e26 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -24,7 +24,7 @@ function Get-Encoding($wc) { } function Get-UserAgent() { - return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64'){'WOW64; '})$PSEdition)" + return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if(${env:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)" } function Show-DeprecatedWarning { @@ -59,6 +59,7 @@ function load_cfg($file) { } function get_config($name, $default) { + $name = $name.ToLowerInvariant() if($null -eq $scoopConfig.$name -and $null -ne $default) { return $default } @@ -72,6 +73,8 @@ function set_config { $value ) + $name = $name.ToLowerInvariant() + if ($null -eq $scoopConfig -or $scoopConfig.Count -eq 0) { ensure (Split-Path -Path $configFile) | Out-Null $scoopConfig = New-Object -TypeName PSObject @@ -98,7 +101,7 @@ function set_config { function setup_proxy() { # note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword' - $proxy = get_config 'proxy' + $proxy = get_config PROXY if(!$proxy) { return } @@ -126,7 +129,7 @@ function setup_proxy() { } function git_cmd { - $proxy = get_config 'proxy' + $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" @@ -153,7 +156,7 @@ 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') { + if ((get_config DEBUG $false) -ine 'true' -and $env:SCOOP_DEBUG -ine 'true') { return } @@ -210,7 +213,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_JUNCTIONS) { + if (get_config NO_JUNCTION) { $version = Select-CurrentVersion -App $app -Global:$global } else { $version = 'current' @@ -246,7 +249,7 @@ function installed_apps($global) { function failed($app, $global) { $app = ($app -split '/|\\')[-1] $appPath = appdir $app $global - $hasCurrent = (get_config NO_JUNCTIONS) -or (Test-Path "$appPath\current") + $hasCurrent = (get_config NO_JUNCTION) -or (Test-Path "$appPath\current") return (Test-Path $appPath) -and !($hasCurrent -and (installed $app $global)) } @@ -394,7 +397,7 @@ function app_status($app, $global) { $status.outdated = $false if ($status.version -and $status.latest_version) { - if (get_config 'force_update' $false) { + if (get_config FORCE_UPDATE $false) { $status.outdated = ((Compare-Version -ReferenceVersion $status.version -DifferenceVersion $status.latest_version) -ne 0) } else { $status.outdated = ((Compare-Version -ReferenceVersion $status.version -DifferenceVersion $status.latest_version) -gt 0) @@ -585,13 +588,6 @@ function Invoke-ExternalCommand { return $true } -function dl($url,$to) { - $wc = New-Object Net.Webclient - $wc.headers.add('Referer', (strip_filename $url)) - $wc.Headers.Add('User-Agent', (Get-UserAgent)) - $wc.downloadFile($url,$to) -} - function env($name,$global,$val='__get') { $target = 'User'; if($global) {$target = 'Machine'} if($val -eq '__get') { [environment]::getEnvironmentVariable($name,$target) } @@ -714,7 +710,7 @@ function shim($path, $global, $name, $arg) { Push-Location $abs_shimdir $relative_path = Resolve-Path -Relative $path Pop-Location - $resolved_path = Resolve-Path $path + $resolved_path = Convert-Path $path if ($path -match '\.(exe|com)$') { # for programs with no awareness of any shell @@ -830,7 +826,7 @@ function shim($path, $global, $name, $arg) { function get_shim_path() { $shim_path = "$(versiondir 'scoop' 'current')\supporting\shims\kiennq\shim.exe" - $shim_version = get_config 'shim' 'default' + $shim_version = get_config SHIM 'default' switch ($shim_version) { '71' { $shim_path = "$(versiondir 'scoop' 'current')\supporting\shims\71\shim.exe"; Break } 'scoopcs' { $shim_path = "$(versiondir 'scoop' 'current')\supporting\shimexe\bin\shim.exe"; Break } @@ -861,15 +857,38 @@ function ensure_in_path($dir, $global) { } } -function ensure_architecture($architecture_opt) { - if(!$architecture_opt) { - return default_architecture +function Get-DefaultArchitecture { + $arch = get_config DEFAULT_ARCHITECTURE + $system = if (${env:ProgramFiles(Arm)}) { + 'arm64' + } elseif ([System.Environment]::Is64BitOperatingSystem) { + '64bit' + } else { + '32bit' } - $architecture_opt = $architecture_opt.ToString().ToLower() - switch($architecture_opt) { - { @('64bit', '64', 'x64', 'amd64', 'x86_64', 'x86-64') -contains $_ } { return '64bit' } - { @('32bit', '32', 'x86', 'i386', '386', 'i686') -contains $_ } { return '32bit' } - default { throw [System.ArgumentException] "Invalid architecture: '$architecture_opt'"} + if ($null -eq $arch) { + $arch = $system + } else { + try { + $arch = Format-ArchitectureString $arch + } catch { + warn 'Invalid default architecture configured. Determining default system architecture' + $arch = $system + } + } + return $arch +} + +function Format-ArchitectureString($Architecture) { + if (!$Architecture) { + return Get-DefaultArchitecture + } + $Architecture = $Architecture.ToString().ToLower() + switch ($Architecture) { + { @('64bit', '64', 'x64', 'amd64', 'x86_64', 'x86-64') -contains $_ } { return '64bit' } + { @('32bit', '32', 'x86', 'i386', '386', 'i686') -contains $_ } { return '32bit' } + { @('arm64', 'arm', 'aarch64') -contains $_ } { return 'arm64' } + default { throw [System.ArgumentException] "Invalid architecture: '$Architecture'" } } } @@ -996,29 +1015,39 @@ function show_app($app, $bucket, $version) { return $app } -function last_scoop_update() { - # PowerShell 6 returns an DateTime Object - $last_update = (get_config lastupdate) - - if ($null -ne $last_update -and $last_update.GetType() -eq [System.String]) { - try { - $last_update = [System.DateTime]::Parse($last_update) - } catch { - $last_update = $null - } - } - return $last_update -} - function is_scoop_outdated() { - $last_update = $(last_scoop_update) $now = [System.DateTime]::Now - if($null -eq $last_update) { - set_config lastupdate $now.ToString('o') - # enforce an update for the first time + try { + $expireHour = (New-TimeSpan (get_config LAST_UPDATE) $now).TotalHours + return ($expireHour -ge 3) + } catch { + # If not System.DateTime + set_config LAST_UPDATE ($now.ToString('o')) | Out-Null return $true } - return $last_update.AddHours(3) -lt $now.ToLocalTime() +} + +function Test-ScoopCoreOnHold() { + $hold_update_until = get_config HOLD_UPDATE_UNTIL + if ($null -eq $hold_update_until) { + return $false + } + $parsed_date = New-Object -TypeName DateTime + if ([System.DateTime]::TryParse($hold_update_until, $null, [System.Globalization.DateTimeStyles]::AssumeLocal, [ref]$parsed_date)) { + if ((New-TimeSpan $parsed_date).TotalSeconds -lt 0) { + warn "Skipping self-update of Scoop Core until $($parsed_date.ToLocalTime())..." + warn "If you want to update Scoop Core immediately, use 'scoop unhold scoop; scoop update'." + return $true + } else { + warn 'Self-update of Scoop Core is enabled again!' + } + } else { + error "'hold_update_until' has been set in the wrong format and was removed." + error 'If you want to disable self-update of Scoop Core for a moment,' + error "use 'scoop hold scoop' or 'scoop config hold_update_until /'." + } + set_config HOLD_UPDATE_UNTIL $null | Out-Null + return $false } function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) { @@ -1086,7 +1115,7 @@ function get_hash([String] $multihash) { } function Get-GitHubToken { - return $env:SCOOP_GH_TOKEN, (get_config 'gh_token') | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1 + return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1 } function handle_special_urls($url) @@ -1186,31 +1215,56 @@ function Out-UTF8File { # for all communication with api.github.com Optimize-SecurityProtocol -# Scoop config file migration +# Load Scoop config $configHome = $env:XDG_CONFIG_HOME, "$env:USERPROFILE\.config" | Select-Object -First 1 $configFile = "$configHome\scoop\config.json" -if ((Test-Path "$env:USERPROFILE\.scoop") -and !(Test-Path $configFile)) { - New-Item -ItemType Directory (Split-Path -Path $configFile) -ErrorAction Ignore | Out-Null - Move-Item "$env:USERPROFILE\.scoop" $configFile - write-host "WARN Scoop configuration has been migrated from '~/.scoop'" -f darkyellow - write-host "WARN to '$configFile'" -f darkyellow -} - -# Load Scoop config $scoopConfig = load_cfg $configFile +# NOTE Scoop config file migration. Remove this after 2023/6/30 +if ($scoopConfig) { + $newConfigNames = @{ + 'lastUpdate' = 'last_update' + 'SCOOP_REPO' = 'scoop_repo' + 'SCOOP_BRANCH' = 'scoop_branch' + '7ZIPEXTRACT_USE_EXTERNAL' = 'use_external_7zip' + 'MSIEXTRACT_USE_LESSMSI' = 'use_lessmsi' + 'NO_JUNCTIONS' = 'no_junction' + 'manifest_review' = 'show_manifest' + 'rootPath' = 'root_path' + 'globalPath' = 'global_path' + 'cachePath' = 'cache_path' + } + $newConfigNames.GetEnumerator() | ForEach-Object { + if ($null -ne $scoopConfig.$($_.Key)) { + $value = $scoopConfig.$($_.Key) + $scoopConfig.PSObject.Properties.Remove($_.Key) + $scoopConfig | Add-Member -MemberType NoteProperty -Name $_.Value -Value $value + if ($_.Key -eq 'lastUpdate') { + $scoopConfigChg = $true + } + } + } + if ($scoopConfigChg) { # Only save config file if there was a change + ConvertTo-Json $scoopConfig | Out-UTF8File -FilePath $configFile + } +} +# END NOTE + # Scoop root directory -$scoopdir = $env:SCOOP, (get_config 'rootPath'), "$env:USERPROFILE\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 +$scoopdir = $env:SCOOP, (get_config ROOT_PATH), "$env:USERPROFILE\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 # Scoop global apps directory -$globaldir = $env:SCOOP_GLOBAL, (get_config 'globalPath'), "$env:ProgramData\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -first 1 +$globaldir = $env:SCOOP_GLOBAL, (get_config GLOBAL_PATH), "$env:ProgramData\scoop" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 # 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 'cachePath'), "$scoopdir\cache" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -first 1 +$cachedir = $env:SCOOP_CACHE, (get_config CACHE_PATH), "$scoopdir\cache" | Where-Object { -not [String]::IsNullOrEmpty($_) } | Select-Object -First 1 + +# OS information +$WindowsBuild = [System.Environment]::OSVersion.Version.Build # Setup proxy globally setup_proxy diff --git a/lib/decompress.ps1 b/lib/decompress.ps1 index 3c4e2cd08e..b56402b1d5 100644 --- a/lib/decompress.ps1 +++ b/lib/decompress.ps1 @@ -18,11 +18,11 @@ function Expand-7zipArchive { [Switch] $Removal ) - if ((get_config 7ZIPEXTRACT_USE_EXTERNAL)) { + if ((get_config USE_EXTERNAL_7ZIP)) { try { $7zPath = (Get-Command '7z' -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source } catch [System.Management.Automation.CommandNotFoundException] { - abort "`nCannot find external 7-Zip (7z.exe) while '7ZIPEXTRACT_USE_EXTERNAL' is 'true'!`nRun 'scoop config 7ZIPEXTRACT_USE_EXTERNAL false' or install 7-Zip manually and try again." + abort "`nCannot find external 7-Zip (7z.exe) while 'use_external_7zip' is 'true'!`nRun 'scoop config use_external_7zip false' or install 7-Zip manually and try again." } } else { $7zPath = Get-HelperPath -Helper 7zip @@ -146,7 +146,7 @@ function Expand-MsiArchive { $OriDestinationPath = $DestinationPath $DestinationPath = "$DestinationPath\_tmp" } - if ((get_config MSIEXTRACT_USE_LESSMSI)) { + if ((get_config USE_LESSMSI)) { $MsiPath = Get-HelperPath -Helper Lessmsi $ArgList = @('x', $Path, "$DestinationPath\") } else { diff --git a/lib/depends.ps1 b/lib/depends.ps1 index bd2b26fb39..9c8d29042d 100644 --- a/lib/depends.ps1 +++ b/lib/depends.ps1 @@ -106,10 +106,10 @@ function Get-InstallationHelper { $installer = arch_specific 'installer' $Manifest $Architecture $post_install = arch_specific 'post_install' $Manifest $Architecture $script = $pre_install + $installer.script + $post_install - if (((Test-7zipRequirement -Uri $url) -or ($script -like '*Expand-7zipArchive *')) -and !(get_config 7ZIPEXTRACT_USE_EXTERNAL)) { + if (((Test-7zipRequirement -Uri $url) -or ($script -like '*Expand-7zipArchive *')) -and !(get_config USE_EXTERNAL_7ZIP)) { $helper += '7zip' } - if (((Test-LessmsiRequirement -Uri $url) -or ($script -like '*Expand-MsiArchive *')) -and (get_config MSIEXTRACT_USE_LESSMSI)) { + if (((Test-LessmsiRequirement -Uri $url) -or ($script -like '*Expand-MsiArchive *')) -and (get_config USE_LESSMSI)) { $helper += 'lessmsi' } if ($Manifest.innosetup -or ($script -like '*Expand-InnoArchive *')) { diff --git a/lib/getopt.ps1 b/lib/getopt.ps1 index 0b04314f10..ce12aae9f5 100644 --- a/lib/getopt.ps1 +++ b/lib/getopt.ps1 @@ -8,6 +8,11 @@ # array of strings that are long-form options. options that take # 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) function getopt($argv, $shortopts, $longopts) { $opts = @{}; $rem = @() @@ -16,29 +21,35 @@ function getopt($argv, $shortopts, $longopts) { } function regex_escape($str) { - return [regex]::escape($str) + return [Regex]::Escape($str) } # ensure these are arrays $argv = @($argv) $longopts = @($longopts) - for($i = 0; $i -lt $argv.length; $i++) { + for ($i = 0; $i -lt $argv.Length; $i++) { $arg = $argv[$i] - if($null -eq $arg) { continue } + if ($null -eq $arg) { continue } # don't try to parse array arguments - if($arg -is [array]) { $rem += ,$arg; continue } - if($arg -is [int]) { $rem += $arg; continue } - if($arg -is [decimal]) { $rem += $arg; continue } + if ($arg -is [Array]) { $rem += , $arg; continue } + if ($arg -is [Int]) { $rem += $arg; continue } + if ($arg -is [Decimal]) { $rem += $arg; continue } - if($arg.startswith('--')) { - $name = $arg.substring(2) + if ($arg -eq '--') { + if ($i -lt $argv.Length - 1) { + $rem += $argv[($i + 1)..($argv.Length - 1)] + } + break + } elseif ($arg.StartsWith('--')) { + $name = $arg.Substring(2) $longopt = $longopts | Where-Object { $_ -match "^$name=?$" } - if($longopt) { - if($longopt.endswith('=')) { # requires arg - if($i -eq $argv.length - 1) { + if ($longopt) { + if ($longopt.EndsWith('=')) { + # requires arg + if ($i -eq $argv.Length - 1) { return err "Option --$name requires an argument." } $opts.$name = $argv[++$i] @@ -48,14 +59,14 @@ function getopt($argv, $shortopts, $longopts) { } else { return err "Option --$name not recognized." } - } elseif($arg.startswith('-') -and $arg -ne '-') { - for($j = 1; $j -lt $arg.length; $j++) { - $letter = $arg[$j].tostring() + } elseif ($arg.StartsWith('-') -and $arg -ne '-') { + for ($j = 1; $j -lt $arg.Length; $j++) { + $letter = $arg[$j].ToString() - if($shortopts -match "$(regex_escape $letter)`:?") { - $shortopt = $matches[0] - if($shortopt[1] -eq ':') { - if($j -ne $arg.length -1 -or $i -eq $argv.length - 1) { + if ($shortopts -match "$(regex_escape $letter)`:?") { + $shortopt = $Matches[0] + if ($shortopt[1] -eq ':') { + if ($j -ne $arg.Length - 1 -or $i -eq $argv.Length - 1) { return err "Option -$letter requires an argument." } $opts.$letter = $argv[++$i] diff --git a/lib/install.ps1 b/lib/install.ps1 index e25bbc4a39..fe5a99f00d 100644 --- a/lib/install.ps1 +++ b/lib/install.ps1 @@ -25,14 +25,15 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru $check_hash = $false } - if(!(supports_architecture $manifest $architecture)) { - write-host -f DarkRed "'$app' doesn't support $architecture architecture!" + $architecture = Get-SupportedArchitecture $manifest $architecture + if ($null -eq $architecture) { + error "'$app' doesn't support current architecture!" return } - if ((get_config 'manifest_review' $false) -and ($MyInvocation.ScriptName -notlike '*scoop-update*')) { + if ((get_config SHOW_MANIFEST $false) -and ($MyInvocation.ScriptName -notlike '*scoop-update*')) { Write-Host "Manifest: $app.json" - $style = get_config cat_style + $style = get_config CAT_STYLE if ($style) { $manifest | ConvertToPrettyJson | bat --no-paging --style $style --language json } else { @@ -49,7 +50,7 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru $original_dir = $dir # keep reference to real (not linked) directory $persist_dir = persistdir $app $global - $fname = dl_urls $app $version $manifest $bucket $architecture $dir $use_cache $check_hash + $fname = Invoke-ScoopDownload $app $version $manifest $bucket $architecture $dir $use_cache $check_hash Invoke-HookScript -HookType 'pre_install' -Manifest $manifest -Arch $architecture run_installer $fname $manifest $architecture $dir $global @@ -80,12 +81,12 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru show_notes $manifest $dir $original_dir $persist_dir } -function dl_with_cache($app, $version, $url, $to, $cookies = $null, $use_cache = $true) { +function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) { $cached = fullpath (cache_path $app $version $url) if(!(test-path $cached) -or !$use_cache) { ensure $cachedir | Out-Null - do_dl $url "$cached.download" $cookies + Start-Download $url "$cached.download" $cookies Move-Item "$cached.download" $cached -force } else { write-host "Loading $(url_remote_filename $url) from cache"} @@ -98,13 +99,13 @@ function dl_with_cache($app, $version, $url, $to, $cookies = $null, $use_cache = } } -function do_dl($url, $to, $cookies) { +function Start-Download ($url, $to, $cookies) { $progress = [console]::isoutputredirected -eq $false -and $host.name -ne 'Windows PowerShell ISE Host' try { $url = handle_special_urls $url - dl $url $to $cookies $progress + Invoke-Download $url $to $cookies $progress } catch { $e = $_.exception if($e.innerexception) { $e = $e.innerexception } @@ -179,7 +180,7 @@ function get_filename_from_metalink($file) { return $filename } -function dl_with_cache_aria2($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) { +function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) { $data = @{} $urls = @(script:url $manifest $architecture) @@ -214,7 +215,7 @@ function dl_with_cache_aria2($app, $version, $manifest, $architecture, $dir, $co $options += "--header='Cookie: $(cookie_header $cookies)'" } - $proxy = get_config 'proxy' + $proxy = get_config PROXY if ($proxy -ne 'none') { if ([Net.Webrequest]::DefaultWebProxy.Address) { $options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'" @@ -355,7 +356,7 @@ function dl_with_cache_aria2($app, $version, $manifest, $architecture, $dir, $co } # download with filesize and progress indicator -function dl($url, $to, $cookies, $progress) { +function Invoke-Download ($url, $to, $cookies, $progress) { $reqUrl = ($url -split '#')[0] $wreq = [Net.WebRequest]::Create($reqUrl) if ($wreq -is [Net.HttpWebRequest]) { @@ -371,7 +372,7 @@ function dl($url, $to, $cookies, $progress) { $wreq.Headers.Add('Cookie', (cookie_header $cookies)) } - get_config 'private_hosts' | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object { + get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object { (ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object { $wreq.Headers[$_.Key] = $_.Value } @@ -409,7 +410,7 @@ function dl($url, $to, $cookies, $progress) { $newUrl = "$newUrl#/$postfix" } - dl $newUrl $to $cookies $progress + Invoke-Download $newUrl $to $cookies $progress return } @@ -420,12 +421,12 @@ function dl($url, $to, $cookies, $progress) { if ($progress -and ($total -gt 0)) { [console]::CursorVisible = $false - function dl_onProgress($read) { - dl_progress $read $total $url + function Trace-DownloadProgress ($read) { + Write-DownloadProgress $read $total $url } } else { write-host "Downloading $url ($(filesize $total))..." - function dl_onProgress { + function Trace-DownloadProgress { #no op } } @@ -437,17 +438,17 @@ function dl($url, $to, $cookies, $progress) { $totalRead = 0 $sw = [diagnostics.stopwatch]::StartNew() - dl_onProgress $totalRead + Trace-DownloadProgress $totalRead while(($read = $s.read($buffer, 0, $buffer.length)) -gt 0) { $fs.write($buffer, 0, $read) $totalRead += $read if ($sw.elapsedmilliseconds -gt 100) { $sw.restart() - dl_onProgress $totalRead + Trace-DownloadProgress $totalRead } } $sw.stop() - dl_onProgress $totalRead + Trace-DownloadProgress $totalRead } finally { if ($progress) { [console]::CursorVisible = $true @@ -463,7 +464,7 @@ function dl($url, $to, $cookies, $progress) { } } -function dl_progress_output($url, $read, $total, $console) { +function Format-DownloadProgress ($url, $read, $total, $console) { $filename = url_remote_filename $url # calculate current percentage done @@ -502,14 +503,14 @@ function dl_progress_output($url, $read, $total, $console) { "$left [$dashes$spaces] $right" } -function dl_progress($read, $total, $url) { +function Write-DownloadProgress ($read, $total, $url) { $console = $host.UI.RawUI; $left = $console.CursorPosition.X; $top = $console.CursorPosition.Y; $width = $console.BufferSize.Width; if($read -eq 0) { - $maxOutputLength = $(dl_progress_output $url 100 $total $console).length + $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 @@ -520,11 +521,11 @@ function dl_progress($read, $total, $url) { } } - write-host $(dl_progress_output $url $read $total $console) -nonewline + write-host $(Format-DownloadProgress $url $read $total $console) -nonewline [console]::SetCursorPosition($left, $top) } -function dl_urls($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) { +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." } @@ -545,13 +546,13 @@ function dl_urls($app, $version, $manifest, $bucket, $architecture, $dir, $use_c # download first if(Test-Aria2Enabled) { - dl_with_cache_aria2 $app $version $manifest $architecture $dir $cookies $use_cache $check_hash + Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash } else { foreach($url in $urls) { $fname = url_filename $url try { - dl_with_cache $app $version $url "$dir\$fname" $cookies $use_cache + Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache } catch { write-host -f darkred $_ abort "URL $url is not valid" @@ -588,7 +589,7 @@ function dl_urls($app, $version, $manifest, $bucket, $architecture, $dir, $use_c $extract_fn = 'Expand-InnoArchive' } elseif($fname -match '\.zip$') { # Use 7zip when available (more fast) - if (((get_config 7ZIPEXTRACT_USE_EXTERNAL) -and (Test-CommandAvailable 7z)) -or (Test-HelperInstalled -Helper 7zip)) { + if (((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z)) -or (Test-HelperInstalled -Helper 7zip)) { $extract_fn = 'Expand-7zipArchive' } else { $extract_fn = 'Expand-ZipArchive' @@ -909,7 +910,7 @@ function rm_shims($app, $manifest, $global, $arch) { # Returns the 'current' junction directory if in use, otherwise # the version directory. function link_current($versiondir) { - if (get_config NO_JUNCTIONS) { return $versiondir.ToString() } + if (get_config NO_JUNCTION) { return $versiondir.ToString() } $currentdir = "$(Split-Path $versiondir)\current" @@ -936,7 +937,7 @@ function link_current($versiondir) { # Returns the 'current' junction directory (if it exists), # otherwise the normal version directory. function unlink_current($versiondir) { - if (get_config NO_JUNCTIONS) { return $versiondir.ToString() } + if (get_config NO_JUNCTION) { return $versiondir.ToString() } $currentdir = "$(Split-Path $versiondir)\current" if (Test-Path $currentdir) { @@ -1046,7 +1047,7 @@ function Invoke-HookScript { [ValidateNotNullOrEmpty()] [PSCustomObject] $Manifest, [Parameter(Mandatory = $true)] - [ValidateSet('32bit', '64bit')] + [ValidateSet('32bit', '64bit', 'arm64')] [String] $Arch ) @@ -1231,14 +1232,16 @@ function persist_permission($manifest, $global) { # test if there are running processes function test_running_process($app, $global) { $processdir = appdir $app $global | Convert-Path - $running_processes = Get-Process | Where-Object { $_.Path -like "$processdir\*" } + $running_processes = Get-Process | Where-Object { $_.Path -like "$processdir\*" } | Out-String if ($running_processes) { - if (get_config 'ignore_running_processes') { - warn "Application `"$app`" is still running. Scoop is configured to ignore this condition." + if (get_config IGNORE_RUNNING_PROCESSES) { + warn "The following instances of `"$app`" are still running. Scoop is configured to ignore this condition." + Write-Host $running_processes return $false } else { - error "Application `"$app`" is still running. Close all instances and try again." + error "The following instances of `"$app`" are still running. Close them and try again." + Write-Host $running_processes return $true } } else { diff --git a/lib/json.ps1 b/lib/json.ps1 index c2ebfb6584..2469204dc5 100644 --- a/lib/json.ps1 +++ b/lib/json.ps1 @@ -98,7 +98,9 @@ function json_path([String] $json, [String] $jsonpath, [Hashtable] $substitution $jsonpath = substitute $jsonpath $substitutions ($jsonpath -like "*=~*") } try { - $obj = [Newtonsoft.Json.Linq.JValue]::Parse($json) + $settings = New-Object -Type Newtonsoft.Json.JsonSerializerSettings + $settings.DateParseHandling = [Newtonsoft.Json.DateParseHandling]::None + $obj = [Newtonsoft.Json.JsonConvert]::DeserializeObject($json, $settings) } catch [Newtonsoft.Json.JsonReaderException] { return $null } diff --git a/lib/manifest.ps1 b/lib/manifest.ps1 index 4a226f7c0a..8b8a685736 100644 --- a/lib/manifest.ps1 +++ b/lib/manifest.ps1 @@ -1,9 +1,9 @@ function manifest_path($app, $bucket) { - fullpath "$(Find-BucketDirectory $bucket)\$(sanitary_path $app).json" + (Get-ChildItem (Find-BucketDirectory $bucket) -Filter "$(sanitary_path $app).json" -Recurse).FullName } function parse_json($path) { - if (!(Test-Path $path)) { return $null } + if ($null -eq $path -or !(Test-Path $path)) { return $null } try { Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop } catch { @@ -102,23 +102,6 @@ function install_info($app, $version, $global) { parse_json $path } -function default_architecture { - $arch = get_config 'default_architecture' - $system = if ([Environment]::Is64BitOperatingSystem) { '64bit' } else { '32bit' } - if ($null -eq $arch) { - $arch = $system - } else { - try { - $arch = ensure_architecture $arch - } catch { - warn 'Invalid default architecture configured. Determining default system architecture' - $arch = $system - } - } - - return $arch -} - function arch_specific($prop, $manifest, $architecture) { if ($manifest.architecture) { $val = $manifest.architecture.$architecture.$prop @@ -128,8 +111,22 @@ function arch_specific($prop, $manifest, $architecture) { if ($manifest.$prop) { return $manifest.$prop } } -function supports_architecture($manifest, $architecture) { - return -not [String]::IsNullOrEmpty((arch_specific 'url' $manifest $architecture)) +function Get-SupportedArchitecture($manifest, $architecture) { + if ($architecture -eq 'arm64' -and ($manifest | ConvertToPrettyJson) -notmatch '[''"]arm64["'']') { + # Windows 10 enables existing unmodified x86 apps to run on Arm devices. + # Windows 11 adds the ability to run unmodified x64 Windows apps on Arm devices! + # Ref: https://learn.microsoft.com/en-us/windows/arm/overview + if ($WindowsBuild -ge 22000) { + # Windows 11 + $architecture = '64bit' + } else { + # Windows 10 + $architecture = '32bit' + } + } + if (![String]::IsNullOrEmpty((arch_specific 'url' $manifest $architecture))) { + return $architecture + } } function generate_user_manifest($app, $bucket, $version) { @@ -145,10 +142,10 @@ function generate_user_manifest($app, $bucket, $version) { abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'" } - ensure $(usermanifestsdir) | out-null + ensure (usermanifestsdir) | out-null try { - Invoke-AutoUpdate $app "$(resolve-path $(usermanifestsdir))" $manifest $version $(@{ }) - return "$(resolve-path $(usermanifest $app))" + Invoke-AutoUpdate $app "$(Convert-Path (usermanifestsdir))\$app.json" $manifest $version $(@{ }) + return Convert-Path (usermanifest $app) } catch { write-host -f darkred "Could not install $app@$version" } diff --git a/lib/psmodules.ps1 b/lib/psmodules.ps1 index dcaf38818f..2fbb6e6525 100644 --- a/lib/psmodules.ps1 +++ b/lib/psmodules.ps1 @@ -23,7 +23,7 @@ function install_psmodule($manifest, $dir, $global) { if (Test-Path $linkfrom) { warn "$(friendly_path $linkfrom) already exists. It will be replaced." - Remove-Item -Path $linkfrom -Force -ErrorAction SilentlyContinue + Remove-Item -Path $linkfrom -Force -Recurse -ErrorAction SilentlyContinue } New-DirectoryJunction $linkfrom $dir | Out-Null @@ -39,8 +39,8 @@ function uninstall_psmodule($manifest, $dir, $global) { $linkfrom = "$modulesdir\$module_name" if (Test-Path $linkfrom) { Write-Host "Removing $(friendly_path $linkfrom)" - $linkfrom = Resolve-Path $linkfrom - Remove-Item -Path $linkfrom -Force -ErrorAction SilentlyContinue + $linkfrom = Convert-Path $linkfrom + Remove-Item -Path $linkfrom -Force -Recurse -ErrorAction SilentlyContinue } } diff --git a/lib/unix.ps1 b/lib/unix.ps1 index a57617dfae..1a202c3eaf 100644 --- a/lib/unix.ps1 +++ b/lib/unix.ps1 @@ -10,14 +10,14 @@ if(!(is_unix)) { } # core.ps1 -$scoopdir = $env:SCOOP, (get_config 'rootPath'), (Join-Path $env:HOME "scoop") | Select-Object -first 1 -$globaldir = $env:SCOOP_GLOBAL, (get_config 'globalPath'), "/usr/local/scoop" | Select-Object -first 1 -$cachedir = $env:SCOOP_CACHE, (get_config 'cachePath'), (Join-Path $scoopdir "cache") | Select-Object -first 1 +$scoopdir = $env:SCOOP, (get_config ROOT_PATH), (Join-Path $env:HOME 'scoop') | Select-Object -First 1 +$globaldir = $env:SCOOP_GLOBAL, (get_config 'GLOBAL_PATH'), '/usr/local/scoop' | Select-Object -First 1 +$cachedir = $env:SCOOP_CACHE, (get_config 'CACHE_PATH'), (Join-Path $scoopdir 'cache') | Select-Object -First 1 # core.ps1 function ensure($dir) { mkdir -p $dir > $null - return resolve-path $dir + return Convert-Path $dir } # install.ps1 diff --git a/lib/versions.ps1 b/lib/versions.ps1 index 5cf21a66f4..60530ec193 100644 --- a/lib/versions.ps1 +++ b/lib/versions.ps1 @@ -50,7 +50,7 @@ function Select-CurrentVersion { # 'manifest.ps1' ) process { $currentPath = "$(appdir $AppName $Global)\current" - if (!(get_config NO_JUNCTIONS)) { + if (!(get_config NO_JUNCTION)) { $currentVersion = (parse_json "$currentPath\manifest.json").version if ($currentVersion -eq 'nightly') { $currentVersion = (Get-Item $currentPath).Target | Split-Path -Leaf diff --git a/libexec/scoop-cat.ps1 b/libexec/scoop-cat.ps1 index ea59d1c73c..0e420bb046 100644 --- a/libexec/scoop-cat.ps1 +++ b/libexec/scoop-cat.ps1 @@ -11,7 +11,7 @@ if (!$app) { error ' missing'; my_usage; exit 1 } $null, $manifest, $bucket, $url = Get-Manifest $app if ($manifest) { - $style = get_config cat_style + $style = get_config CAT_STYLE if ($style) { $manifest | ConvertToPrettyJson | bat --no-paging --style $style --language json } else { diff --git a/libexec/scoop-checkup.ps1 b/libexec/scoop-checkup.ps1 index 1b3c329940..3c6c5f4f50 100644 --- a/libexec/scoop-checkup.ps1 +++ b/libexec/scoop-checkup.ps1 @@ -35,13 +35,13 @@ if (!(Test-HelperInstalled -Helper Dark)) { $globaldir = New-Object System.IO.DriveInfo($globaldir) if ($globaldir.DriveFormat -ne 'NTFS') { - error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP_GLOBAL or 'globalPath' variable in '~/.config/scoop/config.json' to another Drive." + error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP_GLOBAL or 'global_path' variable in '~/.config/scoop/config.json' to another Drive." $issues++ } $scoopdir = New-Object System.IO.DriveInfo($scoopdir) if ($scoopdir.DriveFormat -ne 'NTFS') { - error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP or 'rootPath' variable in '~/.config/scoop/config.json' to another Drive." + error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP or 'root_path' variable in '~/.config/scoop/config.json' to another Drive." $issues++ } diff --git a/libexec/scoop-config.ps1 b/libexec/scoop-config.ps1 index 9ac0de17ea..9395ca5f97 100644 --- a/libexec/scoop-config.ps1 +++ b/libexec/scoop-config.ps1 @@ -21,20 +21,20 @@ # Settings # -------- # -# 7ZIPEXTRACT_USE_EXTERNAL: $true|$false +# use_external_7zip: $true|$false # External 7zip (from path) will be used for archives extraction. # -# MSIEXTRACT_USE_LESSMSI: $true|$false +# use_lessmsi: $true|$false # Prefer lessmsi utility over native msiexec. # -# NO_JUNCTIONS: $true|$false +# no_junction: $true|$false # The 'current' version alias will not be used. Shims and shortcuts will point to specific version instead. # -# SCOOP_REPO: http://github.com/ScoopInstaller/Scoop +# scoop_repo: http://github.com/ScoopInstaller/Scoop # Git repository containining scoop source code. # This configuration is useful for custom forks. # -# SCOOP_BRANCH: master|develop +# scoop_branch: master|develop # Allow to use different branch than master. # Could be used for testing specific functionalities before released into all users. # If you want to receive updates earlier to test new functionalities use develop (see: 'https://github.com/ScoopInstaller/Scoop/issues/2939') @@ -47,7 +47,11 @@ # * An empty or unset value for proxy is equivalent to 'default' (with no username or password) # * To bypass the system proxy and connect directly, use 'none' (with no username or password) # -# default_architecture: 64bit|32bit +# autostash_on_conflict: $true|$false +# When a conflict is detected during updating, Scoop will auto-stash the uncommitted changes. +# (Default is $false, which will abort the update) +# +# default_architecture: 64bit|32bit|arm64 # Allow to configure preferred architecture for application installation. # If not specified, architecture is determined be system. # @@ -60,20 +64,20 @@ # show_update_log: $true|$false # Do not show changed commits on 'scoop update' # -# manifest_review: $true|$false +# show_manifest: $true|$false # Displays the manifest of every app that's about to # be installed, then asks user if they wish to proceed. # # shim: kiennq|scoopcs|71 # Choose scoop shim build. # -# rootPath: $Env:UserProfile\scoop +# root_path: $Env:UserProfile\scoop # Path to Scoop root directory. # -# globalPath: $Env:ProgramData\scoop +# global_path: $Env:ProgramData\scoop # Path to Scoop root directory for global apps. # -# cachePath: +# cache_path: # For downloads, defaults to 'cache' folder under Scoop root directory. # # gh_token: @@ -101,6 +105,12 @@ # For example, if you want to access a private GitHub repository, # you need to add the host to this list with 'match' and 'headers' strings. # +# hold_update_until: +# Disable/Hold Scoop self-updates, until the specified date. +# `scoop hold scoop` will set the value to one day later. +# Should be in the format 'YYYY-MM-DD', 'YYYY/MM/DD' or any other forms that accepted by '[System.DateTime]::Parse()'. +# Ref: https://docs.microsoft.com/dotnet/api/system.datetime.parse?view=netframework-4.5#StringToParse +# # ARIA2 configuration # ------------------- # @@ -137,12 +147,30 @@ 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-depends.ps1 b/libexec/scoop-depends.ps1 index 33d2558089..414d1b7113 100644 --- a/libexec/scoop-depends.ps1 +++ b/libexec/scoop-depends.ps1 @@ -3,16 +3,16 @@ . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency' -. "$PSScriptRoot\..\lib\manifest.ps1" # 'default_architecture' +. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' (indirectly) $opt, $apps, $err = getopt $args 'a:' 'arch=' $app = $apps[0] if(!$app) { error ' missing'; my_usage; exit 1 } -$architecture = default_architecture +$architecture = Get-DefaultArchitecture try { - $architecture = ensure_architecture ($opt.a + $opt.arch) + $architecture = Format-ArchitectureString ($opt.a + $opt.arch) } catch { abort "ERROR: $_" } diff --git a/libexec/scoop-download.ps1 b/libexec/scoop-download.ps1 index 05ba2fa052..66c7632c92 100644 --- a/libexec/scoop-download.ps1 +++ b/libexec/scoop-download.ps1 @@ -14,15 +14,15 @@ # scoop download path\to\app.json # # Options: -# -f, --force Force download (overwrite cache) -# -h, --no-hash-check Skip hash verification (use with caution!) -# -u, --no-update-scoop Don't update Scoop before downloading if it's outdated -# -a, --arch <32bit|64bit> Use the specified architecture, if the app supports it +# -f, --force Force download (overwrite cache) +# -h, --no-hash-check Skip hash verification (use with caution!) +# -u, --no-update-scoop Don't update Scoop before downloading if it's outdated +# -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly) . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) -. "$PSScriptRoot\..\lib\manifest.ps1" # 'default_architecture' 'generate_user_manifest' 'Get-Manifest' +. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' . "$PSScriptRoot\..\lib\install.ps1" $opt, $apps, $err = getopt $args 'fhua:' 'force', 'no-hash-check', 'no-update-scoop', 'arch=' @@ -30,9 +30,9 @@ if ($err) { error "scoop download: $err"; exit 1 } $check_hash = !($opt.h -or $opt.'no-hash-check') $use_cache = !($opt.f -or $opt.force) -$architecture = default_architecture +$architecture = Get-DefaultArchitecture try { - $architecture = ensure_architecture ($opt.a + $opt.arch) + $architecture = Format-ArchitectureString ($opt.a + $opt.arch) } catch { abort "ERROR: $_" } @@ -89,17 +89,18 @@ foreach ($curr_app in $apps) { $curr_check_hash = $false } - if(!(supports_architecture $manifest $architecture)) { - error "'$app' doesn't support $architecture architecture!" + $architecture = Get-SupportedArchitecture $manifest $architecture + if ($null -eq $architecture) { + error "'$app' doesn't support current architecture!" continue } if(Test-Aria2Enabled) { - dl_with_cache_aria2 $app $version $manifest $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash + Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash } else { foreach($url in script:url $manifest $architecture) { try { - dl_with_cache $app $version $url $null $manifest.cookie $use_cache + Invoke-CachedDownload $app $version $url $null $manifest.cookie $use_cache } catch { write-host -f darkred $_ error "URL $url is not valid" diff --git a/libexec/scoop-export.ps1 b/libexec/scoop-export.ps1 index 5975e7eb7a..8eff8db0c5 100644 --- a/libexec/scoop-export.ps1 +++ b/libexec/scoop-export.ps1 @@ -10,7 +10,7 @@ $export = @{} if ($args[0] -eq '-c' -or $args[0] -eq '--config') { $export.config = $scoopConfig # Remove machine-specific properties - foreach ($prop in 'lastUpdate', 'rootPath', 'globalPath', 'cachePath', 'alias') { + foreach ($prop in 'last_update', 'root_path', 'global_path', 'cache_path', 'alias') { $export.config.PSObject.Properties.Remove($prop) } } diff --git a/libexec/scoop-hold.ps1 b/libexec/scoop-hold.ps1 index ec4f8ce070..2d52310c79 100644 --- a/libexec/scoop-hold.ps1 +++ b/libexec/scoop-hold.ps1 @@ -32,6 +32,12 @@ if ($global -and !(is_admin)) { $apps | ForEach-Object { $app = $_ + if ($app -eq 'scoop') { + $hold_update_until = [System.DateTime]::Now.AddDays(1) + set_config HOLD_UPDATE_UNTIL $hold_update_until.ToString('o') | Out-Null + success "$app is now held and might not be updated until $($hold_update_until.ToLocalTime())." + return + } if (!(installed $app $global)) { if ($global) { error "'$app' is not installed globally." @@ -41,7 +47,7 @@ $apps | ForEach-Object { return } - if (get_config NO_JUNCTIONS) { + if (get_config NO_JUNCTION){ $version = Select-CurrentVersion -App $app -Global:$global } else { $version = 'current' diff --git a/libexec/scoop-import.ps1 b/libexec/scoop-import.ps1 index b6a0ba5d6b..7e3ec1c52c 100644 --- a/libexec/scoop-import.ps1 +++ b/libexec/scoop-import.ps1 @@ -13,7 +13,7 @@ param( $import = $null $bucket_names = @() -$def_arch = default_architecture +$def_arch = Get-DefaultArchitecture if (Test-Path $scoopfile) { $import = parse_json $scoopfile @@ -40,12 +40,12 @@ foreach ($item in $import.apps) { } else { '' } - $arch = if ('64bit' -in $info -and '32bit' -eq $def_arch) { + $arch = if ('64bit' -in $info) { ' --arch 64bit' - } elseif ('32bit' -in $info -and '64bit' -eq $def_arch) { + } elseif ('32bit' -in $info) { ' --arch 32bit' } else { - '' + ' --arch arm64' } $app = if ($item.Source -in $bucket_names) { diff --git a/libexec/scoop-info.ps1 b/libexec/scoop-info.ps1 index 74f7aa931a..f4346a6025 100644 --- a/libexec/scoop-info.ps1 +++ b/libexec/scoop-info.ps1 @@ -158,7 +158,7 @@ if ($status.installed) { if ($verbose) { # Get download size if app not installed $totalPackage = 0 - foreach ($url in @(url $manifest (default_architecture))) { + foreach ($url in @(url $manifest (Get-DefaultArchitecture))) { try { if (Test-Path (fullpath (cache_path $app $manifest.version $url))) { $cached = " (latest version is cached)" diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1 index 4865ea2848..994bb5b4cf 100644 --- a/libexec/scoop-install.ps1 +++ b/libexec/scoop-install.ps1 @@ -14,17 +14,17 @@ # scoop install \path\to\app.json # # Options: -# -g, --global Install the app globally -# -i, --independent Don't install dependencies automatically -# -k, --no-cache Don't use the download cache -# -u, --no-update-scoop Don't update Scoop before installing if it's outdated -# -s, --skip Skip hash validation (use with caution!) -# -a, --arch <32bit|64bit> Use the specified architecture, if the app supports it +# -g, --global Install the app globally +# -i, --independent Don't install dependencies automatically +# -k, --no-cache Don't use the download cache +# -u, --no-update-scoop Don't update Scoop before installing if it's outdated +# -s, --skip Skip hash validation (use with caution!) +# -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it . "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' 'manifest.ps1' (indirectly) . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) -. "$PSScriptRoot\..\lib\manifest.ps1" # 'default_architecture' 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly) +. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly) . "$PSScriptRoot\..\lib\install.ps1" . "$PSScriptRoot\..\lib\decompress.ps1" . "$PSScriptRoot\..\lib\shortcuts.ps1" @@ -39,9 +39,9 @@ $global = $opt.g -or $opt.global $check_hash = !($opt.s -or $opt.skip) $independent = $opt.i -or $opt.independent $use_cache = !($opt.k -or $opt.'no-cache') -$architecture = default_architecture +$architecture = Get-DefaultArchitecture try { - $architecture = ensure_architecture ($opt.a + $opt.arch) + $architecture = Format-ArchitectureString ($opt.a + $opt.arch) } catch { abort "ERROR: $_" } diff --git a/libexec/scoop-list.ps1 b/libexec/scoop-list.ps1 index 0d681d36d3..da44dd1410 100644 --- a/libexec/scoop-list.ps1 +++ b/libexec/scoop-list.ps1 @@ -6,7 +6,7 @@ param($query) . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' . "$PSScriptRoot\..\lib\manifest.ps1" # 'parse_json' 'Select-CurrentVersion' (indirectly) -$def_arch = default_architecture +$def_arch = Get-DefaultArchitecture if (-not (Get-FormatData ScoopApps)) { Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml" } diff --git a/libexec/scoop-reset.ps1 b/libexec/scoop-reset.ps1 index f84328bcbe..e10ef9e4f9 100644 --- a/libexec/scoop-reset.ps1 +++ b/libexec/scoop-reset.ps1 @@ -63,7 +63,7 @@ $apps | ForEach-Object { write-host "Resetting $app ($version)." - $dir = resolve-path (versiondir $app $version $global) + $dir = Convert-Path (versiondir $app $version $global) $original_dir = $dir $persist_dir = persistdir $app $global diff --git a/libexec/scoop-shim.ps1 b/libexec/scoop-shim.ps1 index ab14dfa683..41263afe6a 100644 --- a/libexec/scoop-shim.ps1 +++ b/libexec/scoop-shim.ps1 @@ -1,18 +1,18 @@ -# Usage: scoop shim [] [ [...]] [-g(lobal)] +# Usage: scoop shim [...] [options] [other_args] # Summary: Manipulate Scoop shims -# Help: Manipulate Scoop shims: add, rm, list, info, alter, etc. +# Help: Available subcommands: add, rm, list, info, alter. # # To add a custom shim, use the 'add' subcommand: # # scoop shim add [...] # -# To remove a shim, use the 'rm' subcommand (CAUTION: this could remove shims added by an app manifest): +# To remove shims, use the 'rm' subcommand: (CAUTION: this could remove shims added by an app manifest) # -# scoop shim rm +# scoop shim rm [...] # # 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: # @@ -23,60 +23,40 @@ # scoop shim alter # # Options: -# -g(lobal) Add/Remove/Info/Alter global shim(s) -# (NOTICE: USING SINGLE DASH) -# (HINT: To pass arguments like '-g' or '-global' to the shim, use quotes) +# -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 -param($SubCommand, $ShimName, [Switch]$global) +param($SubCommand) +. "$PSScriptRoot\..\lib\getopt.ps1" . "$PSScriptRoot\..\lib\install.ps1" # for rm_shim if ($SubCommand -notin @('add', 'rm', 'list', 'info', 'alter')) { - 'ERROR: must be one of: add, rm, list, info, alter' + if (!$SubCommand) { + error ' missing' + } else { + error "'$SubCommand' is not one of available subcommands: add, rm, list, info, alter" + } my_usage exit 1 } -if ($SubCommand -ne 'list' -and !$ShimName) { - "ERROR: must be specified for subcommand '$SubCommand'" +$opt, $other, $err = getopt $Args 'g' 'global' +if ($err) { "scoop shim: $err"; exit 1 } + +$global = $opt.g -or $opt.global + +if ($SubCommand -ne 'list' -and $other.Length -eq 0) { + error " must be specified for subcommand '$SubCommand'" my_usage exit 1 } -if ($Args) { - switch ($SubCommand) { - 'add' { - if ($Args[0] -like '-*') { - "ERROR: must be specified for subcommand 'add'" - my_usage - exit 1 - } else { - if (($Args -join ' ') -match "^'(.*?)'\s*(.*?)$") { - $commandPath = $Matches[1] - $commandArgs = $Matches[2] - } else { - $commandPath = $Args[0] - if ($Args.Length -gt 1) { - $commandArgs = $Args[1..($Args.Length - 1)] - } - } - } - } - 'rm' { - $ShimName = @($ShimName) + $Args - } - 'list' { - $ShimName = (@($ShimName) + $Args) -join '|' - } - default { - # For 'info' and 'alter' - "ERROR: Option $Args not recognized." - my_usage - exit 1 - } - } -} - if (-not (Get-FormatData ScoopShims)) { Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml" } @@ -110,6 +90,16 @@ function Get-ShimPath($ShimName, $Global) { switch ($SubCommand) { 'add' { + if ($other.Length -lt 2 -or $other[1] -eq '') { + error " must be specified for subcommand 'add'" + my_usage + exit 1 + } + $shimName = $other[0] + $commandPath = $other[1] + if ($other.Length -gt 2) { + $commandArgs = $other[2..($other.Length - 1)] + } if ($commandPath -notmatch '[\\/]') { $shortPath = $commandPath $commandPath = Get-ShimTarget (Get-ShimPath $shortPath $global) @@ -126,12 +116,14 @@ switch ($SubCommand) { Write-Host '...' shim $commandPath $global $shimName $commandArgs } else { - abort "ERROR: '$($Args[0])' does not exist" 3 + Write-Host "ERROR: Command path does not exist: " -ForegroundColor Red -NoNewline + Write-Host $($other[1]) -ForegroundColor Cyan + exit 3 } } 'rm' { $failed = @() - $ShimName | ForEach-Object { + $other | ForEach-Object { if (Get-ShimPath $_ $global) { rm_shim $_ (shimdir $global) } else { @@ -139,61 +131,82 @@ switch ($SubCommand) { } } if ($failed) { - Write-Host 'Shims not found: ' -NoNewline - Write-Host $failed -ForegroundColor Cyan + $failed | ForEach-Object { + Write-Host "ERROR: $(if ($global) { 'Global' } else {'Local' }) shim not found: " -ForegroundColor Red -NoNewline + Write-Host $_ -ForegroundColor Cyan + } exit 3 } } 'list' { - $shims = Get-ChildItem -Path $localShimDir -Recurse -Include '*.shim', '*.ps1' | - Where-Object { !$ShimName -or ($_.BaseName -match $ShimName) } | - Select-Object -ExpandProperty FullName + $other = @($other) -ne '*' + # Validate all given patterns before matching. + $other | ForEach-Object { + try { + $pattern = $_ + [Regex]::New($pattern) + } catch { + Write-Host "ERROR: Invalid pattern: " -ForegroundColor Red -NoNewline + Write-Host $pattern -ForegroundColor Magenta + exit 1 + } + } + $pattern = $other -join '|' + $shims = @() + if (!$global) { + $shims += Get-ChildItem -Path $localShimDir -Recurse -Include '*.shim', '*.ps1' | + Where-Object { !$pattern -or ($_.BaseName -match $pattern) } | + Select-Object -ExpandProperty FullName + } if (Test-Path $globalShimDir) { $shims += Get-ChildItem -Path $globalShimDir -Recurse -Include '*.shim', '*.ps1' | - Where-Object { !$ShimName -or ($_.BaseName -match $ShimName) } | + Where-Object { !$pattern -or ($_.BaseName -match $pattern) } | Select-Object -ExpandProperty FullName } $shims.ForEach({ Get-ShimInfo $_ }) | Add-Member -TypeName 'ScoopShims' -PassThru } 'info' { - $shimPath = Get-ShimPath $ShimName $global + $shimName = $other[0] + $shimPath = Get-ShimPath $shimName $global if ($shimPath) { Get-ShimInfo $shimPath } else { - Write-Host "$(if ($global) { 'Global' } else { 'Local' }) shim not found: " -NoNewline - Write-Host $ShimName -ForegroundColor Cyan - if (Get-ShimPath $ShimName (!$global)) { + Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline + Write-Host $shimName -ForegroundColor Cyan + 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 "run 'scoop shim info $shimName$(if (!$global) { ' --global' })' to show its info" exit 2 } exit 3 } } 'alter' { - $shimPath = Get-ShimPath $ShimName $global + $shimName = $other[0] + $shimPath = Get-ShimPath $shimName $global if ($shimPath) { $shimInfo = Get-ShimInfo $shimPath if ($null -eq $shimInfo.Alternatives) { - Write-Host 'No alternatives of ' -NoNewline - Write-Host $ShimName -ForegroundColor Cyan -NoNewline - Write-Host ' found.' + Write-Host 'ERROR: No alternatives of ' -ForegroundColor Red -NoNewline + Write-Host $shimName -ForegroundColor Cyan -NoNewline + Write-Host ' found.' -ForegroundColor Red exit 2 } $shimInfo.Alternatives = $shimInfo.Alternatives.Split(' ') [System.Management.Automation.Host.ChoiceDescription[]]$altApps = 1..$shimInfo.Alternatives.Length | ForEach-Object { - New-Object System.Management.Automation.Host.ChoiceDescription "&$($_)`b$($shimInfo.Alternatives[$_ - 1])", "Sets '$ShimName' shim from $($shimInfo.Alternatives[$_ - 1])." + New-Object System.Management.Automation.Host.ChoiceDescription "&$($_)`b$($shimInfo.Alternatives[$_ - 1])", "Sets '$shimName' shim from $($shimInfo.Alternatives[$_ - 1])." } - $selected = $Host.UI.PromptForChoice("Alternatives of '$ShimName' command", "Please choose one that provides '$ShimName' as default:", $altApps, 0) + $selected = $Host.UI.PromptForChoice("Alternatives of '$shimName' command", "Please choose one that provides '$shimName' as default:", $altApps, 0) if ($selected -eq 0) { - Write-Host $ShimName -ForegroundColor Cyan -NoNewline + 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.' } else { $newApp = $shimInfo.Alternatives[$selected] Write-Host 'Use ' -NoNewline - Write-Host $ShimName -ForegroundColor Cyan -NoNewline + Write-Host $shimName -ForegroundColor Cyan -NoNewline Write-Host ' from ' -NoNewline Write-Host $newApp -ForegroundColor DarkYellow -NoNewline Write-Host ' as default...' -NoNewline @@ -211,11 +224,11 @@ switch ($SubCommand) { Write-Host 'done.' } } else { - Write-Host "$(if ($global) { 'Global' } else { 'Local' }) shim not found: " -NoNewline - Write-Host $ShimName -ForegroundColor Cyan - if (Get-ShimPath $ShimName (!$global)) { + Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline + Write-Host $shimName -ForegroundColor Cyan + 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 "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 2aa93d2df8..e678247972 100644 --- a/libexec/scoop-unhold.ps1 +++ b/libexec/scoop-unhold.ps1 @@ -32,6 +32,11 @@ if ($global -and !(is_admin)) { $apps | ForEach-Object { $app = $_ + if ($app -eq 'scoop') { + set_config HOLD_UPDATE_UNTIL $null | Out-Null + success "$app is no longer held and can be updated again." + return + } if (!(installed $app $global)) { if ($global) { error "'$app' is not installed globally." @@ -41,7 +46,7 @@ $apps | ForEach-Object { return } - if (get_config NO_JUNCTIONS) { + if (get_config NO_JUNCTION){ $version = Select-CurrentVersion -App $app -Global:$global } else { $version = 'current' diff --git a/libexec/scoop-update.ps1 b/libexec/scoop-update.ps1 index ca502dd7ce..84bd448f78 100644 --- a/libexec/scoop-update.ps1 +++ b/libexec/scoop-update.ps1 @@ -54,16 +54,18 @@ if(($PSVersionTable.PSVersion.Major) -lt 5) { Write-Output "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows" break } +$show_update_log = get_config SHOW_UPDATE_LOG $true + +function update_scoop($show_update_log) { + # Test if Scoop Core is hold + if(Test-ScoopCoreOnHold) { + return + } -function update_scoop() { # check for git - if(!(Test-CommandAvailable git)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." } + if (!(Test-CommandAvailable git)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." } Write-Host "Updating Scoop..." - $last_update = $(last_scoop_update) - if ($null -eq $last_update) {$last_update = [System.DateTime]::Now} - $last_update = $last_update.ToString('s') - $show_update_log = get_config 'show_update_log' $true $currentdir = fullpath $(versiondir 'scoop' 'current') if (!(Test-Path "$currentdir\.git")) { $newdir = "$currentdir\..\new" @@ -98,6 +100,17 @@ function update_scoop() { $isRepoChanged = !($currentRepo -match $configRepo) $isBranchChanged = !($currentBranch -match "\*\s+$configBranch") + # Stash uncommitted changes + if (git -C "$currentdir" 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 + } else { + warn "Uncommitted changes detected. Update aborted." + return + } + } + # Change remote url if the repo is changed if ($isRepoChanged) { git -C "$currentdir" config remote.origin.url "$configRepo" @@ -135,6 +148,11 @@ function update_scoop() { # } 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." } foreach ($bucket in Get-LocalBucket) { Write-Host "Updating '$bucket' bucket..." @@ -165,9 +183,6 @@ function update_scoop() { 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" } } - - set_config lastupdate ([System.DateTime]::Now.ToString('o')) | Out-Null - success 'Scoop was updated successfully!' } function update($app, $global, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) { @@ -176,7 +191,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c $install = install_info $app $old_version $global # re-use architecture, bucket and url from first install - $architecture = ensure_architecture $install.architecture + $architecture = Format-ArchitectureString $install.architecture $bucket = $install.bucket if ($null -eq $bucket) { $bucket = 'main' @@ -210,12 +225,12 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c # Remove and replace whole region after proper fix Write-Host "Downloading new version" if (Test-Aria2Enabled) { - dl_with_cache_aria2 $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash + Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash } else { $urls = script:url $manifest $architecture foreach ($url in $urls) { - dl_with_cache $app $version $url $null $manifest.cookie $true + Invoke-CachedDownload $app $version $url $null $manifest.cookie $true if ($check_hash) { $manifest_hash = hash_for_url $manifest $url $architecture @@ -262,6 +277,8 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c # directory. $refdir = unlink_current $dir + uninstall_psmodule $old_manifest $refdir $global + if ($force -and ($old_version -eq $version)) { if (!(Test-Path "$dir/../_$version.old")) { Move-Item "$dir" "$dir/../_$version.old" @@ -304,7 +321,10 @@ if (-not ($apps -or $all)) { error 'scoop update: --no-cache is invalid when is not specified.' exit 1 } - update_scoop + update_scoop $show_update_log + update_bucket $show_update_log + set_config LAST_UPDATE ([System.DateTime]::Now.ToString('o')) | Out-Null + success 'Scoop was updated successfully!' } else { if ($global -and !(is_admin)) { 'ERROR: You need admin rights to update global apps.'; exit 1 @@ -316,7 +336,10 @@ if (-not ($apps -or $all)) { $apps_param = $apps if ($updateScoop) { - update_scoop + update_scoop $show_update_log + update_bucket $show_update_log + set_config LAST_UPDATE ([System.DateTime]::Now.ToString('o')) | Out-Null + success 'Scoop was updated successfully!' } if ($apps_param -eq '*' -or $all) { diff --git a/libexec/scoop-virustotal.ps1 b/libexec/scoop-virustotal.ps1 index ef4b84e2f6..f22bbbf1c0 100644 --- a/libexec/scoop-virustotal.ps1 +++ b/libexec/scoop-virustotal.ps1 @@ -37,7 +37,7 @@ $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 } -$architecture = ensure_architecture +$architecture = Format-ArchitectureString if (is_scoop_outdated) { if ($opt.u -or $opt.'no-update-scoop') { @@ -66,7 +66,7 @@ $_ERR_NO_API_KEY = 16 $exit_code = 0 # Global API key: -$api_key = get_config virustotal_api_key +$api_key = get_config VIRUSTOTAL_API_KEY if (!$api_key) { abort ("VirusTotal API key is not configured`n" + " You could get one from https://www.virustotal.com/gui/my-apikey and set with`n" + diff --git a/schema.json b/schema.json index 25f4ce6eac..a9c30d3b4e 100644 --- a/schema.json +++ b/schema.json @@ -179,19 +179,11 @@ "type": "array" }, "autoupdateArch": { + "type": "object", "additionalProperties": false, "properties": { - "url": { - "$ref": "#/definitions/autoupdateUriOrArrayOfAutoupdateUris" - }, - "hash": { - "$ref": "#/definitions/hashExtractionOrArrayOfHashExtractions" - }, - "extract_dir": { - "$ref": "#/definitions/stringOrArrayOfStrings" - }, - "extract_to": { - "$ref": "#/definitions/stringOrArrayOfStrings" + "bin": { + "$ref": "#/definitions/stringOrArrayOfStringsOrAnArrayOfArrayOfStrings" }, "env_add_path": { "$ref": "#/definitions/stringOrArrayOfStrings" @@ -199,71 +191,85 @@ "env_set": { "type": "object" }, - "bin": { - "$ref": "#/definitions/stringOrArrayOfStringsOrAnArrayOfArrayOfStrings" + "extract_dir": { + "$ref": "#/definitions/stringOrArrayOfStrings" }, - "shortcuts": { - "$ref": "#/definitions/shortcutsArray" + "hash": { + "$ref": "#/definitions/hashExtractionOrArrayOfHashExtractions" }, "installer": { + "type": "object", "additionalProperties": false, "properties": { "file": { "type": "string" } - }, - "type": "object" + } }, - "post_install": { - "$ref": "#/definitions/stringOrArrayOfStrings" + "shortcuts": { + "$ref": "#/definitions/shortcutsArray" }, - "psmodule": { + "url": { + "$ref": "#/definitions/autoupdateUriOrArrayOfAutoupdateUris" + } + } + }, + "autoupdate": { + "type": "object", + "additionalProperties": false, + "properties": { + "architecture": { + "type": "object", "additionalProperties": false, "properties": { - "name": { - "type": "string" + "32bit": { + "$ref": "#/definitions/autoupdateArch" + }, + "64bit": { + "$ref": "#/definitions/autoupdateArch" + }, + "arm64": { + "$ref": "#/definitions/autoupdateArch" } - }, - "type": "object" + } }, - "persist": { + "bin": { "$ref": "#/definitions/stringOrArrayOfStringsOrAnArrayOfArrayOfStrings" }, + "env_add_path": { + "$ref": "#/definitions/stringOrArrayOfStrings" + }, + "env_set": { + "type": "object" + }, + "extract_dir": { + "$ref": "#/definitions/stringOrArrayOfStrings" + }, + "hash": { + "$ref": "#/definitions/hashExtractionOrArrayOfHashExtractions" + }, "license": { "$ref": "#/definitions/license" }, "notes": { "$ref": "#/definitions/stringOrArrayOfStrings" - } - }, - "type": "object" - }, - "autoupdate": { - "anyOf": [ - { - "$ref": "#/definitions/autoupdateArch" }, - { + "persist": { + "$ref": "#/definitions/stringOrArrayOfStringsOrAnArrayOfArrayOfStrings" + }, + "psmodule": { + "type": "object", + "additionalProperties": false, "properties": { - "notes": { - "$ref": "#/definitions/stringOrArrayOfStrings" - }, - "architecture": { - "type": "object", - "additionalProperties": false, - "properties": { - "32bit": { - "$ref": "#/definitions/autoupdateArch" - }, - "64bit": { - "$ref": "#/definitions/autoupdateArch" - } - } + "name": { + "type": "string" } } + }, + "url": { + "$ref": "#/definitions/autoupdateUriOrArrayOfAutoupdateUris" } - ], - "type": "object" + } }, "checkver": { "anyOf": [ @@ -315,6 +321,25 @@ "script": { "$ref": "#/definitions/stringOrArrayOfStrings", "description": "Custom PowerShell script to retrieve application version using more complex approach." + }, + "sourceforge": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "project": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "type": "object" + } + ] } }, "type": "object" @@ -508,6 +533,9 @@ }, "64bit": { "$ref": "#/definitions/architecture" + }, + "arm64": { + "$ref": "#/definitions/architecture" } }, "type": "object" @@ -614,6 +642,34 @@ "type": "string" } }, + "if": { + "properties": { + "architecture": { + "properties": { + "64bit": { + "properties": { + "url": false + } + }, + "32bit": { + "properties": { + "url": false + } + }, + "arm64": { + "properties": { + "url": false + } + } + } + } + } + }, + "then": { + "required": [ + "url" + ] + }, "required": [ "version", "homepage", diff --git a/test/Import-Bucket-Tests.ps1 b/test/Import-Bucket-Tests.ps1 index bdc249f800..b42b3bdc7f 100644 --- a/test/Import-Bucket-Tests.ps1 +++ b/test/Import-Bucket-Tests.ps1 @@ -82,7 +82,7 @@ Describe 'manifest validates against the schema' -Tag 'Manifests' { } $changed_manifests = (Get-GitChangedFile -Path $repo_dir -Include '*.json' -Commit $commit) } - $manifest_files = Get-ChildItem $bucketdir *.json + $manifest_files = Get-ChildItem $bucketdir -Filter '*.json' -Recurse $validator = New-Object Scoop.Validator($schema, $true) } diff --git a/test/Import-File-Tests.ps1 b/test/Import-File-Tests.ps1 index 0359e8069f..5aadcd3ade 100644 --- a/test/Import-File-Tests.ps1 +++ b/test/Import-File-Tests.ps1 @@ -66,7 +66,7 @@ Describe 'Style constraints for non-binary project files' { It 'file newlines are CRLF' -Skip:$(-not $files_exist) { $badFiles = @( foreach ($file in $files) { - $content = Get-Content -Raw $file.FullName + $content = [System.IO.File]::ReadAllText($file.FullName) if (!$content) { throw "File contents are null: $($file.FullName)" } diff --git a/test/Scoop-Core.Tests.ps1 b/test/Scoop-Core.Tests.ps1 index 75d6b7d7df..398cbfaad2 100644 --- a/test/Scoop-Core.Tests.ps1 +++ b/test/Scoop-Core.Tests.ps1 @@ -397,3 +397,40 @@ Describe 'app' -Tag 'Scoop' { $version | Should -Be '1.8.0-rc2' } } + +Describe 'Format Architecture String' -Tag 'Scoop' { + It 'should keep correct architectures' { + Format-ArchitectureString '32bit' | Should -Be '32bit' + Format-ArchitectureString '32' | Should -Be '32bit' + Format-ArchitectureString 'x86' | Should -Be '32bit' + Format-ArchitectureString 'X86' | Should -Be '32bit' + Format-ArchitectureString 'i386' | Should -Be '32bit' + Format-ArchitectureString '386' | Should -Be '32bit' + Format-ArchitectureString 'i686' | Should -Be '32bit' + + Format-ArchitectureString '64bit' | Should -Be '64bit' + Format-ArchitectureString '64' | Should -Be '64bit' + Format-ArchitectureString 'x64' | Should -Be '64bit' + Format-ArchitectureString 'X64' | Should -Be '64bit' + Format-ArchitectureString 'amd64' | Should -Be '64bit' + Format-ArchitectureString 'AMD64' | Should -Be '64bit' + Format-ArchitectureString 'x86_64' | Should -Be '64bit' + Format-ArchitectureString 'x86-64' | Should -Be '64bit' + + Format-ArchitectureString 'arm64' | Should -Be 'arm64' + Format-ArchitectureString 'arm' | Should -Be 'arm64' + Format-ArchitectureString 'aarch64' | Should -Be 'arm64' + Format-ArchitectureString 'ARM64' | Should -Be 'arm64' + Format-ArchitectureString 'ARM' | Should -Be 'arm64' + Format-ArchitectureString 'AARCH64' | Should -Be 'arm64' + } + + It 'should fallback to the default architecture on empty input' { + Format-ArchitectureString '' | Should -Be $(Get-DefaultArchitecture) + Format-ArchitectureString $null | Should -Be $(Get-DefaultArchitecture) + } + + It 'should show an error with an invalid architecture' { + { Format-ArchitectureString 'PPC' } | Should -Throw "Invalid architecture: 'ppc'" + } +} diff --git a/test/Scoop-Depends.Tests.ps1 b/test/Scoop-Depends.Tests.ps1 index 03713c0c3e..b465d28220 100644 --- a/test/Scoop-Depends.Tests.ps1 +++ b/test/Scoop-Depends.Tests.ps1 @@ -46,14 +46,14 @@ Describe 'Package Dependencies' -Tag 'Scoop' { Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -Be @('7zip') } It 'Helpers reflect config changes' { - Mock get_config { $false } -ParameterFilter { $name -eq 'MSIEXTRACT_USE_LESSMSI' } - Mock get_config { $true } -ParameterFilter { $name -eq '7ZIPEXTRACT_USE_EXTERNAL' } + Mock get_config { $false } -ParameterFilter { $name -eq 'USE_LESSMSI' } + Mock get_config { $true } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' } Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -BeNullOrEmpty Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -BeNullOrEmpty } It 'Not return installed helpers' { - Mock get_config { $true } -ParameterFilter { $name -eq 'MSIEXTRACT_USE_LESSMSI' } - Mock get_config { $false } -ParameterFilter { $name -eq '7ZIPEXTRACT_USE_EXTERNAL' } + Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' } + Mock get_config { $false } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' } Mock Test-HelperInstalled { $true }-ParameterFilter { $Helper -eq '7zip' } Mock Test-HelperInstalled { $false }-ParameterFilter { $Helper -eq 'Lessmsi' } Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -Be @('lessmsi') @@ -68,7 +68,7 @@ Describe 'Package Dependencies' -Tag 'Scoop' { Context 'Dependencies resolution' { BeforeAll { Mock Test-HelperInstalled { $false } - Mock get_config { $true } -ParameterFilter { $name -eq 'MSIEXTRACT_USE_LESSMSI' } + Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' } Mock Get-Manifest { 'lessmsi', @{}, $null, $null } -ParameterFilter { $app -eq 'lessmsi' } Mock Get-Manifest { '7zip', @{ url = 'test.msi' }, $null, $null } -ParameterFilter { $app -eq '7zip' } Mock Get-Manifest { 'innounp', @{}, $null, $null } -ParameterFilter { $app -eq 'innounp' } diff --git a/test/Scoop-Format-Manifest.Tests.ps1 b/test/Scoop-Format-Manifest.Tests.ps1 deleted file mode 100644 index 25bc9730ce..0000000000 --- a/test/Scoop-Format-Manifest.Tests.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -. "$PSScriptRoot\Scoop-TestLib.ps1" -. "$PSScriptRoot\..\lib\json.ps1" -. "$PSScriptRoot\..\lib\manifest.ps1" - -Describe 'Pretty json formating' -Tag 'Scoop' { - BeforeAll { - $format = "$PSScriptRoot\fixtures\format" - $manifests = Get-ChildItem "$format\formatted" -File -Filter '*.json' - } - - Context 'Beautify manifest' { - $manifests | ForEach-Object { - if ($PSVersionTable.PSVersion.Major -gt 5) { $_ = $_.Name } # Fix for pwsh - - It "$_" { - $pretty_json = (parse_json "$format\unformatted\$_") | ConvertToPrettyJson - $correct = (Get-Content "$format\formatted\$_") -join "`r`n" - $correct.CompareTo($pretty_json) | Should -Be 0 - } - } - } -} diff --git a/test/Scoop-GetOpts.Tests.ps1 b/test/Scoop-GetOpts.Tests.ps1 index d9c67ef91a..2fcbf56fe6 100644 --- a/test/Scoop-GetOpts.Tests.ps1 +++ b/test/Scoop-GetOpts.Tests.ps1 @@ -68,4 +68,18 @@ Describe 'getopt' -Tag 'Scoop' { $err | Should -BeNullOrEmpty $opt.'long-arg' | Should -Be 'test' } + + It 'handles the option terminator' { + $opt, $rem, $err = getopt '--long-arg', '--' '' 'long-arg' + $err | Should -BeNullOrEmpty + $opt.'long-arg' | Should -BeTrue + $rem[0] | Should -BeNullOrEmpty + $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 + $rem[0] | Should -Be '-x' + $rem[1] | Should -Be '-y' + } } diff --git a/test/Scoop-Install.Tests.ps1 b/test/Scoop-Install.Tests.ps1 index 8750feb506..3559ac89e8 100644 --- a/test/Scoop-Install.Tests.ps1 +++ b/test/Scoop-Install.Tests.ps1 @@ -6,37 +6,6 @@ $isUnix = is_unix -Describe 'ensure_architecture' -Tag 'Scoop' { - It 'should keep correct architectures' { - ensure_architecture '32bit' | Should -Be '32bit' - ensure_architecture '32' | Should -Be '32bit' - ensure_architecture 'x86' | Should -Be '32bit' - ensure_architecture 'X86' | Should -Be '32bit' - ensure_architecture 'i386' | Should -Be '32bit' - ensure_architecture '386' | Should -Be '32bit' - ensure_architecture 'i686' | Should -Be '32bit' - - ensure_architecture '64bit' | Should -Be '64bit' - ensure_architecture '64' | Should -Be '64bit' - ensure_architecture 'x64' | Should -Be '64bit' - ensure_architecture 'X64' | Should -Be '64bit' - ensure_architecture 'amd64' | Should -Be '64bit' - ensure_architecture 'AMD64' | Should -Be '64bit' - ensure_architecture 'x86_64' | Should -Be '64bit' - ensure_architecture 'x86-64' | Should -Be '64bit' - } - - It 'should fallback to the default architecture on empty input' { - ensure_architecture '' | Should -Be $(default_architecture) - ensure_architecture $null | Should -Be $(default_architecture) - } - - It 'should show an error with an invalid architecture' { - { ensure_architecture 'PPC' } | Should -Throw - { ensure_architecture 'PPC' } | Should -Throw "Invalid architecture: 'ppc'" - } -} - Describe 'appname_from_url' -Tag 'Scoop' { It 'should extract the correct name' { appname_from_url 'https://example.org/directory/foobar.json' | Should -Be 'foobar' diff --git a/test/Scoop-Manifest.Tests.ps1 b/test/Scoop-Manifest.Tests.ps1 new file mode 100644 index 0000000000..02a24162ab --- /dev/null +++ b/test/Scoop-Manifest.Tests.ps1 @@ -0,0 +1,49 @@ +. "$PSScriptRoot\Scoop-TestLib.ps1" +. "$PSScriptRoot\..\lib\json.ps1" +. "$PSScriptRoot\..\lib\manifest.ps1" + +Describe 'Pretty json formating' -Tag 'Scoop' { + BeforeAll { + $format = "$PSScriptRoot\fixtures\format" + $manifests = Get-ChildItem "$format\formatted" -File -Filter '*.json' + } + + Context 'Beautify manifest' { + $manifests | ForEach-Object { + if ($PSVersionTable.PSVersion.Major -gt 5) { $_ = $_.Name } # Fix for pwsh + + It "$_" { + $pretty_json = (parse_json "$format\unformatted\$_") | ConvertToPrettyJson + $correct = (Get-Content "$format\formatted\$_") -join "`r`n" + $correct.CompareTo($pretty_json) | Should -Be 0 + } + } + } +} + +Describe 'Handle ARM64 and correctly fallback' -Tag 'Scoop' { + It 'Should return "arm64" if supported' { + $manifest1 = @{ url = 'test'; architecture = @{ 'arm64' = @{ pre_install = 'test' } } } + $manifest2 = @{ url = 'test'; pre_install = "'arm64'" } + $manifest3 = @{ architecture = @{ 'arm64' = @{ url = 'test' } } } + Get-SupportedArchitecture $manifest1 'arm64' | Should -Be 'arm64' + Get-SupportedArchitecture $manifest2 'arm64' | Should -Be 'arm64' + Get-SupportedArchitecture $manifest3 'arm64' | Should -Be 'arm64' + } + It 'Should return "64bit" if unsupported on Windows 11' { + $WindowsBuild = 22000 + $manifest1 = @{ url = 'test' } + $manifest2 = @{ architecture = @{ '64bit' = @{ url = 'test' } } } + Get-SupportedArchitecture $manifest1 'arm64' | Should -Be '64bit' + Get-SupportedArchitecture $manifest2 'arm64' | Should -Be '64bit' + } + It 'Should return "32bit" if unsupported on Windows 10' { + $WindowsBuild = 19044 + $manifest2 = @{ url = 'test' } + $manifest1 = @{ url = 'test'; architecture = @{ '64bit' = @{ pre_install = 'test' } } } + $manifest3 = @{ architecture = @{ '64bit' = @{ url = 'test' } } } + Get-SupportedArchitecture $manifest1 'arm64' | Should -Be '32bit' + Get-SupportedArchitecture $manifest2 'arm64' | Should -Be '32bit' + Get-SupportedArchitecture $manifest3 'arm64' | Should -BeNullOrEmpty + } +} diff --git a/test/bin/test.ps1 b/test/bin/test.ps1 index 9a2689c7cb..f70805aded 100644 --- a/test/bin/test.ps1 +++ b/test/bin/test.ps1 @@ -3,7 +3,7 @@ #Requires -Modules @{ ModuleName = 'Pester'; MaximumVersion = '4.99' } #Requires -Modules @{ ModuleName = 'PSScriptAnalyzer'; ModuleVersion = '1.17.1' } param( - [String] $TestPath = $(Resolve-Path "$PSScriptRoot\..\") + [String] $TestPath = $(Convert-Path "$PSScriptRoot\..\") ) $splat = @{ @@ -21,7 +21,7 @@ if ($env:CI -eq $true) { $commitMessage = $env:BHCommitMessage # Check if tests are called from the Core itself, if so, adding excludes - if ($TestPath -eq $(Resolve-Path "$PSScriptRoot\..\")) { + if ($TestPath -eq $(Convert-Path "$PSScriptRoot\..\")) { if ($commitMessage -match '!linter') { Write-Warning "Skipping code linting per commit flag '!linter'" $excludes += 'Linter'