mirror of
https://github.com/ScoopInstaller/Scoop.git
synced 2025-10-30 14:17:54 +00:00
Compare commits
112 Commits
v0.4.0
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d46f67e34 | ||
|
|
4c55e7aebd | ||
|
|
db8d554f80 | ||
|
|
b592b38abd | ||
|
|
25d35007c6 | ||
|
|
1fae80b2c2 | ||
|
|
cf345e272e | ||
|
|
99fb8383b7 | ||
|
|
0a52f0d0de | ||
|
|
04b7ce79c7 | ||
|
|
becd429581 | ||
|
|
d3f681ce03 | ||
|
|
ff38652e01 | ||
|
|
c9f0728ff7 | ||
|
|
691c23330c | ||
|
|
9f62e55862 | ||
|
|
b588a06e41 | ||
|
|
9246250808 | ||
|
|
8c51a50856 | ||
|
|
ebd8c036fa | ||
|
|
ed6101a2ab | ||
|
|
26a03e2404 | ||
|
|
f9c4f9e175 | ||
|
|
3321b969a4 | ||
|
|
22f1672fcb | ||
|
|
887f79d2d7 | ||
|
|
5bae77eebb | ||
|
|
210b5e45a0 | ||
|
|
3aa64baeb1 | ||
|
|
0470ba2d04 | ||
|
|
398aaa2330 | ||
|
|
4585f50a54 | ||
|
|
9a6cdc7328 | ||
|
|
039d2d77c2 | ||
|
|
ec44c6c7b2 | ||
|
|
c264afc6bd | ||
|
|
4061bff270 | ||
|
|
3e3ce096b8 | ||
|
|
83696854a8 | ||
|
|
82a35323d3 | ||
|
|
dfadfcc1dc | ||
|
|
0ade4a5344 | ||
|
|
ce95819fa5 | ||
|
|
c7795b44af | ||
|
|
4e528e1655 | ||
|
|
ac6f1baaf4 | ||
|
|
dc9d198014 | ||
|
|
84e00fdb77 | ||
|
|
7a309a1b00 | ||
|
|
3577f91d82 | ||
|
|
e0c682de7c | ||
|
|
79cf33d0b7 | ||
|
|
7f99c499d7 | ||
|
|
859d1db51b | ||
|
|
c7ec5c82b2 | ||
|
|
46624b00c9 | ||
|
|
49ee8ad6ec | ||
|
|
5dc5dd22f8 | ||
|
|
5971f1dda8 | ||
|
|
0cb479abf0 | ||
|
|
429ba7af5b | ||
|
|
be56faf290 | ||
|
|
69edc6dc54 | ||
|
|
0138dc4266 | ||
|
|
1c271f5b90 | ||
|
|
a76884af19 | ||
|
|
7cc4faea1d | ||
|
|
83f25a4673 | ||
|
|
42af27d16c | ||
|
|
716b6db3ae | ||
|
|
ade7aa4a15 | ||
|
|
7e0a2a28c6 | ||
|
|
2d02483fb8 | ||
|
|
93359a43a1 | ||
|
|
d8b3cc8a6c | ||
|
|
9239c26bbd | ||
|
|
3a39ba048f | ||
|
|
a9ca75c0cd | ||
|
|
d20819e147 | ||
|
|
dec4232754 | ||
|
|
fa1b42bf28 | ||
|
|
700a2f4f5e | ||
|
|
c9048ad978 | ||
|
|
5c896e901f | ||
|
|
8ea37387ae | ||
|
|
2544745695 | ||
|
|
5ce70c4139 | ||
|
|
2d50a02a32 | ||
|
|
f6f46f6cf4 | ||
|
|
d337bb1fa6 | ||
|
|
cddcd98e33 | ||
|
|
86d3fef187 | ||
|
|
e324fa4c5f | ||
|
|
d9c5c90285 | ||
|
|
82640c456e | ||
|
|
4dd2cfdc5f | ||
|
|
a5bd2297c6 | ||
|
|
b710ff6c0a | ||
|
|
1dd479f0be | ||
|
|
1752050614 | ||
|
|
edaae8d39f | ||
|
|
776135e0ab | ||
|
|
cc863353e2 | ||
|
|
5b86c302e5 | ||
|
|
b8580aa931 | ||
|
|
23d55ced72 | ||
|
|
36026f1804 | ||
|
|
3b34497eb4 | ||
|
|
bb88dfb2d4 | ||
|
|
d285bb08d4 | ||
|
|
eac58400db | ||
|
|
2dd91d5ba3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ test/installer/tmp/*
|
||||
test/tmp/*
|
||||
*~
|
||||
TestResults.xml
|
||||
supporting/sqlite/*
|
||||
|
||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -1,3 +1,113 @@
|
||||
## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/v0.5.3...develop)
|
||||
|
||||
### Features
|
||||
|
||||
- **install:** Add support for historical manifests ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370))
|
||||
- **scoop-uninstall**: Allow access to `$bucket` in uninstall scripts ([#6380](https://github.com/ScoopInstaller/Scoop/issues/6380))
|
||||
- **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scoop-update**: Force sync tags w/ remote branch while scoop update ([#6439](https://github.com/ScoopInstaller/Scoop/issues/6439))
|
||||
- **scoop-download:** Fix function `nightly_version` not defined error ([#6386](https://github.com/ScoopInstaller/Scoop/issues/6386))
|
||||
- **autoupdate:** Use origin URL to handle URLs with fragment in GitHub mode ([#6455](https://github.com/ScoopInstaller/Scoop/issues/6455))
|
||||
- **buckets|scoop-info:** Switch git log date format to ISO 8601 to avoid locale issues ([#6446](https://github.com/ScoopInstaller/Scoop/issues/6446))
|
||||
- **scoop-version:** Fix logic error caused by missing brackets ([#6463](https://github.com/ScoopInstaller/Scoop/issues/6463))
|
||||
- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
|
||||
|
||||
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11
|
||||
|
||||
### Features
|
||||
|
||||
- **autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scoop-download|install|update:** Fallback to default downloader when aria2 fails ([#4292](https://github.com/ScoopInstaller/Scoop/issues/4292))
|
||||
- **decompress**: `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
|
||||
- **decompress**: Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config with USE_EXTERNAL_7ZIP ([#6327](https://github.com/ScoopInstaller/Scoop/issues/6327))
|
||||
- **commands**: Handling broken aliases ([#6141](https://github.com/ScoopInstaller/Scoop/issues/6141))
|
||||
- **shim:** Do not suppress `stderr`, properly check `wslpath`/`cygpath` command first ([#6114](https://github.com/ScoopInstaller/Scoop/issues/6114))
|
||||
- **scoop-bucket:** Add missing import for `no_junction` envs ([#6181](https://github.com/ScoopInstaller/Scoop/issues/6181))
|
||||
- **scoop-uninstall:** Fix uninstaller does not gain Global state ([#6430](https://github.com/ScoopInstaller/Scoop/issues/6430))
|
||||
- **scoop-depends-tests:** Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true ([#6431](https://github.com/ScoopInstaller/Scoop/issues/6431))
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
- **download:** Move download-related functions to 'download.ps1' ([#6095](https://github.com/ScoopInstaller/Scoop/issues/6095))
|
||||
- **Get-Manifest:** Select actual source for manifest ([#6142](https://github.com/ScoopInstaller/Scoop/issues/6142))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- **shim:** Update kiennq-shim to v3.1.2 ([#6261](https://github.com/ScoopInstaller/Scoop/issues/6261))
|
||||
|
||||
## [v0.5.2](https://github.com/ScoopInstaller/Scoop/compare/v0.5.1...v0.5.2) - 2024-07-26
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scoop-alias:** Fix 'Option --verbose not recognized.' ([#6062](https://github.com/ScoopInstaller/Scoop/issues/6062))
|
||||
- **scoop-hold:** Use 'foreach' loop to allow 'continue' statement ([#6078](https://github.com/ScoopInstaller/Scoop/issues/6078))
|
||||
- **core:** Use 'Join-Path' to construct cache file path ([#6079](https://github.com/ScoopInstaller/Scoop/issues/6079))
|
||||
- **json:** Don't serialize jsonpath return if only one result ([#6066](https://github.com/ScoopInstaller/Scoop/issues/6066), [#6073](https://github.com/ScoopInstaller/Scoop/issues/6073))
|
||||
|
||||
### Builds
|
||||
|
||||
- **supporting:** Update Json.Schema to 4.0.1 ([#6072](https://github.com/ScoopInstaller/Scoop/issues/6072))
|
||||
|
||||
## [v0.5.1](https://github.com/ScoopInstaller/Scoop/compare/v0.5.0...v0.5.1) - 2024-07-16
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scoop-alias:** Pass options correctly ([#6003](https://github.com/ScoopInstaller/Scoop/issues/6003))
|
||||
- **scoop-virustotal:** Adjust `json_path` parameters to retrieve correct analysis stats ([#6044](https://github.com/ScoopInstaller/Scoop/issues/6044))
|
||||
- **bucket:** Implement error handling for failed bucket addition ([#6051](https://github.com/ScoopInstaller/Scoop/issues/6051))
|
||||
- **database:** Fix compatibility with Windows PowerShell ([#6045](https://github.com/ScoopInstaller/Scoop/issues/6045))
|
||||
- **install:** Expand `env_set` items before setting Environment Variables ([#6050](https://github.com/ScoopInstaller/Scoop/issues/6050))
|
||||
- **install:** Fix parsing error when installing multiple apps w/ specific version ([#6039](https://github.com/ScoopInstaller/Scoop/issues/6039))
|
||||
|
||||
## [v0.5.0](https://github.com/ScoopInstaller/Scoop/compare/v0.4.2...v0.5.0) - 2024-07-01
|
||||
|
||||
### Features
|
||||
|
||||
- **scoop-search:** Use SQLite for caching apps to speed up local search ([#5851](https://github.com/ScoopInstaller/Scoop/issues/5851), [#5918](https://github.com/ScoopInstaller/Scoop/issues/5918), [#5946](https://github.com/ScoopInstaller/Scoop/issues/5946), [#5949](https://github.com/ScoopInstaller/Scoop/issues/5949), [#5955](https://github.com/ScoopInstaller/Scoop/issues/5955), [#5966](https://github.com/ScoopInstaller/Scoop/issues/5966), [#5967](https://github.com/ScoopInstaller/Scoop/issues/5967), [#5981](https://github.com/ScoopInstaller/Scoop/issues/5981))
|
||||
- **core:** New cache filename format ([#5929](https://github.com/ScoopInstaller/Scoop/issues/5929), [#5944](https://github.com/ScoopInstaller/Scoop/issues/5944))
|
||||
- **decompress:** Use innounp-unicode as default Inno Setup Unpacker ([#6028](https://github.com/ScoopInstaller/Scoop/issues/6028))
|
||||
- **install:** Added the ability to install specific version of app from URL/file link ([#5988](https://github.com/ScoopInstaller/Scoop/issues/5988))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scoop-download|install|update:** Use consistent options ([#5956](https://github.com/ScoopInstaller/Scoop/issues/5956))
|
||||
- **scoop-info:** Fix download size estimating ([#5958](https://github.com/ScoopInstaller/Scoop/issues/5958))
|
||||
- **scoop-search:** Catch error of parsing invalid manifest ([#5930](https://github.com/ScoopInstaller/Scoop/issues/5930))
|
||||
- **checkver:** Correct variable 'regex' to 'regexp' ([#5993](https://github.com/ScoopInstaller/Scoop/issues/5993))
|
||||
- **checkver:** Correct error messages ([#6024](https://github.com/ScoopInstaller/Scoop/issues/6024))
|
||||
- **core:** Search for Git executable instead of any cmdlet ([#5998](https://github.com/ScoopInstaller/Scoop/issues/5998))
|
||||
- **core:** Use correct path in 'bash' ([#6006](https://github.com/ScoopInstaller/Scoop/issues/6006))
|
||||
- **core:** Limit the number of commands to get when search for git executable ([#6013](https://github.com/ScoopInstaller/Scoop/issues/6013))
|
||||
- **decompress:** Match `extract_dir`/`extract_to` and archives ([#5983](https://github.com/ScoopInstaller/Scoop/issues/5983))
|
||||
- **json:** Serialize jsonpath return ([#5921](https://github.com/ScoopInstaller/Scoop/issues/5921))
|
||||
- **shim:** Restore original path for JAR cmd ([#6030](https://github.com/ScoopInstaller/Scoop/issues/6030))
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
- **decompress:** Use 7zip to extract Zstd archive ([#5973](https://github.com/ScoopInstaller/Scoop/issues/5973))
|
||||
- **install:** Separate archive extraction from downloader ([#5951](https://github.com/ScoopInstaller/Scoop/issues/5951))
|
||||
- **install:** Replace 'run_(un)installer()' with 'Invoke-Installer()' ([#5968](https://github.com/ScoopInstaller/Scoop/issues/5968), [#5971](https://github.com/ScoopInstaller/Scoop/issues/5971))
|
||||
|
||||
## [v0.4.2](https://github.com/ScoopInstaller/Scoop/compare/v0.4.1...v0.4.2) - 2024-05-14
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **autoupdate:** Copy `PSCustomObject`-type properties within `substitute()` to prevent reference changes ([#5934](https://github.com/ScoopInstaller/Scoop/issues/5934), [#5962](https://github.com/ScoopInstaller/Scoop/issues/5962))
|
||||
- **core:** Fix `Invoke-ExternalCommand` quoting rules ([#5945](https://github.com/ScoopInstaller/Scoop/issues/5945))
|
||||
- **system:** Fix argument passing to `Split-PathLikeEnvVar()` in deprecated `strip_path()` ([#5937](https://github.com/ScoopInstaller/Scoop/issues/5937))
|
||||
|
||||
## [v0.4.1](https://github.com/ScoopInstaller/Scoop/compare/v0.4.0...v0.4.1) - 2024-04-25
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** Fix `Invoke-ExternalCommand` regression ([#5923](https://github.com/ScoopInstaller/Scoop/issues/5923))
|
||||
|
||||
## [v0.4.0](https://github.com/ScoopInstaller/Scoop/compare/v0.3.1...v0.4.0) - 2024-04-18
|
||||
|
||||
### Features
|
||||
|
||||
@@ -46,7 +46,7 @@ param(
|
||||
. "$PSScriptRoot\..\lib\autoupdate.ps1"
|
||||
. "$PSScriptRoot\..\lib\json.ps1"
|
||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
|
||||
$Dir = Convert-Path $Dir
|
||||
if ($ForceUpdate) { $Update = $true }
|
||||
|
||||
@@ -28,7 +28,7 @@ param(
|
||||
|
||||
. "$PSScriptRoot\..\lib\core.ps1"
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
|
||||
$Dir = Convert-Path $Dir
|
||||
$Queue = @()
|
||||
|
||||
@@ -73,7 +73,7 @@ param(
|
||||
. "$PSScriptRoot\..\lib\buckets.ps1"
|
||||
. "$PSScriptRoot\..\lib\json.ps1"
|
||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1" # needed for hash generation
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
|
||||
if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) {
|
||||
$Dir = Split-Path $App
|
||||
@@ -275,7 +275,7 @@ while ($in_progress -gt 0) {
|
||||
$ver = $Version
|
||||
|
||||
if (!$ver) {
|
||||
if (!$regex -and $replace) {
|
||||
if (!$regexp -and $replace) {
|
||||
next "'replace' requires 're' or 'regex'"
|
||||
continue
|
||||
}
|
||||
@@ -294,13 +294,15 @@ while ($in_progress -gt 0) {
|
||||
}
|
||||
$page = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd()
|
||||
}
|
||||
$source = $url
|
||||
if ($script) {
|
||||
$page = Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
|
||||
$source = 'the output of script'
|
||||
}
|
||||
|
||||
if ($jsonpath) {
|
||||
# Return only a single value if regex is absent
|
||||
$noregex = [String]::IsNullOrEmpty($regex)
|
||||
$noregex = [String]::IsNullOrEmpty($regexp)
|
||||
# If reverse is ON and regex is ON,
|
||||
# Then reverse would have no effect because regex handles reverse
|
||||
# on its own
|
||||
@@ -310,7 +312,7 @@ while ($in_progress -gt 0) {
|
||||
$ver = json_path_legacy $page $jsonpath
|
||||
}
|
||||
if (!$ver) {
|
||||
next "couldn't find '$jsonpath' in $url"
|
||||
next "couldn't find '$jsonpath' in $source"
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -332,7 +334,7 @@ while ($in_progress -gt 0) {
|
||||
# Getting version from XML, using XPath
|
||||
$ver = $xml.SelectSingleNode($xpath, $nsmgr).'#text'
|
||||
if (!$ver) {
|
||||
next "couldn't find '$($xpath -replace 'ns:', '')' in $url"
|
||||
next "couldn't find '$($xpath -replace 'ns:', '')' in $source"
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -348,31 +350,31 @@ while ($in_progress -gt 0) {
|
||||
}
|
||||
|
||||
if ($regexp) {
|
||||
$regex = New-Object System.Text.RegularExpressions.Regex($regexp)
|
||||
$re = New-Object System.Text.RegularExpressions.Regex($regexp)
|
||||
if ($reverse) {
|
||||
$match = $regex.Matches($page) | Select-Object -Last 1
|
||||
$match = $re.Matches($page) | Select-Object -Last 1
|
||||
} else {
|
||||
$match = $regex.Matches($page) | Select-Object -First 1
|
||||
$match = $re.Matches($page) | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($match -and $match.Success) {
|
||||
$matchesHashtable = @{}
|
||||
$regex.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
|
||||
$re.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
|
||||
$ver = $matchesHashtable['1']
|
||||
if ($replace) {
|
||||
$ver = $regex.Replace($match.Value, $replace)
|
||||
$ver = $re.Replace($match.Value, $replace)
|
||||
}
|
||||
if (!$ver) {
|
||||
$ver = $matchesHashtable['version']
|
||||
}
|
||||
} else {
|
||||
next "couldn't match '$regexp' in $url"
|
||||
next "couldn't match '$regexp' in $source"
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ver) {
|
||||
next "couldn't find new version in $url"
|
||||
next "couldn't find new version in $source"
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ param(
|
||||
. "$PSScriptRoot\..\lib\core.ps1"
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||
. "$PSScriptRoot\..\lib\description.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
|
||||
$Dir = Convert-Path $Dir
|
||||
$Queue = @()
|
||||
|
||||
@@ -20,8 +20,8 @@ switch ($subCommand) {
|
||||
}
|
||||
({ $subCommand -in @('-v', '--version') }) {
|
||||
Write-Host 'Current Scoop version:'
|
||||
if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') {
|
||||
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline')
|
||||
if ((Test-GitAvailable) -and (Test-Path "$PSScriptRoot\..\.git") -and ((get_config SCOOP_BRANCH 'master') -ne 'master')) {
|
||||
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
|
||||
} else {
|
||||
$version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md"
|
||||
Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline
|
||||
@@ -31,9 +31,9 @@ switch ($subCommand) {
|
||||
|
||||
Get-LocalBucket | ForEach-Object {
|
||||
$bucketLoc = Find-BucketDirectory $_ -Root
|
||||
if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) {
|
||||
if ((Test-GitAvailable) -and (Test-Path "$bucketLoc\.git")) {
|
||||
Write-Host "'$_' bucket:"
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline')
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
|
||||
Write-Host ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function do_uninstall($app, $global) {
|
||||
$architecture = $install.architecture
|
||||
|
||||
Write-Output "Uninstalling '$app'"
|
||||
run_uninstaller $manifest $architecture $dir
|
||||
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall
|
||||
rm_shims $app $manifest $global $architecture
|
||||
|
||||
# If a junction was used during install, that will have been used
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
# Must included with 'json.ps1'
|
||||
|
||||
function format_hash([String] $hash) {
|
||||
$hash = $hash.toLower()
|
||||
|
||||
# Workaround for GitHub API:
|
||||
# `"digest": "sha256:<SHA256_STRING>"`
|
||||
if ($hash -like 'sha256:*') {
|
||||
$hash = $hash.Substring(7) # Remove prefix 'sha256:'
|
||||
}
|
||||
|
||||
switch ($hash.Length) {
|
||||
32 { $hash = "md5:$hash" } # md5
|
||||
40 { $hash = "sha1:$hash" } # sha1
|
||||
64 { $hash = $hash } # sha256
|
||||
128 { $hash = "sha512:$hash" } # sha512
|
||||
default { $hash = $null }
|
||||
}
|
||||
return $hash
|
||||
}
|
||||
|
||||
function find_hash_in_rdf([String] $url, [String] $basename) {
|
||||
$xml = $null
|
||||
try {
|
||||
@@ -191,13 +211,14 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
|
||||
$hash = $null
|
||||
|
||||
$hashmode = $config.mode
|
||||
$originurl = strip_fragment $url
|
||||
$basename = [System.Web.HttpUtility]::UrlDecode((url_remote_filename($url)))
|
||||
|
||||
$substitutions = $substitutions.Clone()
|
||||
$substitutions.Add('$url', (strip_fragment $url))
|
||||
$substitutions.Add('$baseurl', (strip_filename (strip_fragment $url)).TrimEnd('/'))
|
||||
$substitutions.Add('$url', $originurl)
|
||||
$substitutions.Add('$baseurl', (strip_filename $originurl).TrimEnd('/'))
|
||||
$substitutions.Add('$basename', $basename)
|
||||
$substitutions.Add('$urlNoExt', (strip_ext (strip_fragment $url)))
|
||||
$substitutions.Add('$urlNoExt', (strip_ext $originurl))
|
||||
$substitutions.Add('$basenameNoExt', (strip_ext $basename))
|
||||
|
||||
debug $substitutions
|
||||
@@ -246,6 +267,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
|
||||
$hashmode = 'sourceforge'
|
||||
}
|
||||
|
||||
if (!$hashfile_url -and $url -match 'https:\/\/github\.com\/(?<owner>[^\/]+)\/(?<repo>[^\/]+)\/releases\/download\/[^\/]+\/[^\/]+') {
|
||||
$hashmode = 'github'
|
||||
}
|
||||
|
||||
switch ($hashmode) {
|
||||
'extract' {
|
||||
$hash = find_hash_in_textfile $hashfile_url $substitutions $regex
|
||||
@@ -273,6 +298,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
|
||||
$hashfile_url = (strip_filename (strip_fragment "https://sourceforge.net/projects/$($matches['project'])/files/$($matches['file'])")).TrimEnd('/')
|
||||
$hash = find_hash_in_textfile $hashfile_url $substitutions '"$basename":.*?"sha1":\s*"([a-fA-F0-9]{40})"'
|
||||
}
|
||||
'github' {
|
||||
$hashfile_url = "https://api.github.com/repos/$($matches['owner'])/$($matches['repo'])/releases"
|
||||
$hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $originurl + "')].digest")
|
||||
}
|
||||
}
|
||||
|
||||
if ($hash) {
|
||||
|
||||
@@ -108,7 +108,7 @@ function list_buckets {
|
||||
$path = Find-BucketDirectory $_ -Root
|
||||
if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
$bucket.Source = Invoke-Git -Path $path -ArgumentList @('config', 'remote.origin.url')
|
||||
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aD', '-n', '1') | Get-Date
|
||||
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aI', '-n', '1') | Get-Date
|
||||
} else {
|
||||
$bucket.Source = friendly_path $path
|
||||
$bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime
|
||||
@@ -154,8 +154,17 @@ function add_bucket($name, $repo) {
|
||||
}
|
||||
ensure $bucketsdir | Out-Null
|
||||
$dir = ensure $dir
|
||||
Invoke-Git -ArgumentList @('clone', $repo, $dir, '-q')
|
||||
$out = Invoke-Git -ArgumentList @('clone', $repo, $dir, '-q')
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
error "Failed to clone '$repo' to '$dir'.`n`nError given:`n$out`n`nPlease check the repository URL or network connection and try again."
|
||||
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
return 1
|
||||
}
|
||||
Write-Host 'OK'
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
info 'Updating cache...'
|
||||
Set-ScoopDB -Path (Get-ChildItem (Find-BucketDirectory $name) -Filter '*.json' -Recurse).FullName
|
||||
}
|
||||
success "The $name bucket was added successfully."
|
||||
return 0
|
||||
}
|
||||
@@ -168,6 +177,11 @@ function rm_bucket($name) {
|
||||
}
|
||||
|
||||
Remove-Item $dir -Recurse -Force -ErrorAction Stop
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
info 'Updating cache...'
|
||||
Remove-ScoopDBItem -Bucket $name
|
||||
}
|
||||
success "The $name bucket was removed successfully."
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
100
lib/commands.ps1
100
lib/commands.ps1
@@ -1,6 +1,10 @@
|
||||
# Description: Functions for managing commands and aliases.
|
||||
|
||||
## Functions for commands
|
||||
|
||||
function command_files {
|
||||
(Get-ChildItem "$PSScriptRoot\..\libexec") + (Get-ChildItem "$scoopdir\shims") |
|
||||
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
|
||||
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
|
||||
}
|
||||
|
||||
function commands {
|
||||
@@ -19,11 +23,10 @@ function command_path($cmd) {
|
||||
# get path from shim
|
||||
$shim_path = "$scoopdir\shims\scoop-$cmd.ps1"
|
||||
$line = ((Get-Content $shim_path) | Where-Object { $_.startswith('$path') })
|
||||
if($line) {
|
||||
if ($line) {
|
||||
Invoke-Command ([scriptblock]::Create($line)) -NoNewScope
|
||||
$cmd_path = $path
|
||||
}
|
||||
else { $cmd_path = $shim_path }
|
||||
} else { $cmd_path = $shim_path }
|
||||
}
|
||||
|
||||
$cmd_path
|
||||
@@ -34,3 +37,92 @@ function exec($cmd, $arguments) {
|
||||
|
||||
& $cmd_path @arguments
|
||||
}
|
||||
|
||||
## Functions for aliases
|
||||
|
||||
function add_alias {
|
||||
param(
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$name,
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$command,
|
||||
[string]$description
|
||||
)
|
||||
|
||||
$aliases = get_config ALIAS ([PSCustomObject]@{})
|
||||
if ($aliases.$name) {
|
||||
abort "Alias '$name' already exists."
|
||||
}
|
||||
|
||||
$alias_script_name = "scoop-$name"
|
||||
$shimdir = shimdir $false
|
||||
if (Test-Path "$shimdir\$alias_script_name.ps1") {
|
||||
abort "File '$alias_script_name.ps1' already exists in shims directory."
|
||||
}
|
||||
$script = @(
|
||||
"# Summary: $description",
|
||||
"$command"
|
||||
) -join "`n"
|
||||
try {
|
||||
$script | Out-UTF8File "$shimdir\$alias_script_name.ps1"
|
||||
} catch {
|
||||
abort $_.Exception
|
||||
}
|
||||
|
||||
# Add the new alias to the config.
|
||||
$aliases | Add-Member -MemberType NoteProperty -Name $name -Value $alias_script_name
|
||||
set_config ALIAS $aliases | Out-Null
|
||||
}
|
||||
|
||||
function rm_alias {
|
||||
param(
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$name
|
||||
)
|
||||
|
||||
$aliases = get_config ALIAS ([PSCustomObject]@{})
|
||||
if (!$aliases.$name) {
|
||||
abort "Alias '$name' doesn't exist."
|
||||
}
|
||||
|
||||
info "Removing alias '$name'..."
|
||||
if (Test-Path "$(shimdir $false)\scoop-$name.ps1") {
|
||||
Remove-Item "$(shimdir $false)\scoop-$name.ps1"
|
||||
}
|
||||
$aliases.PSObject.Properties.Remove($name)
|
||||
set_config ALIAS $aliases | Out-Null
|
||||
}
|
||||
|
||||
function list_aliases {
|
||||
param(
|
||||
[bool]$verbose
|
||||
)
|
||||
|
||||
$aliases = get_config ALIAS ([PSCustomObject]@{})
|
||||
$alias_info = $aliases.PSObject.Properties.Name | Where-Object { $_ } | ForEach-Object {
|
||||
# Mark the alias as <BROKEN>, if the alias script file does NOT exist.
|
||||
if (!(Test-Path "$(shimdir $false)\scoop-$_.ps1")) {
|
||||
[PSCustomObject]@{
|
||||
Name = $_
|
||||
Command = '<BROKEN>'
|
||||
}
|
||||
return
|
||||
}
|
||||
$content = Get-Content (command_path $_)
|
||||
[PSCustomObject]@{
|
||||
Name = $_
|
||||
Command = ($content | Select-Object -Skip 1).Trim()
|
||||
Summary = (summary $content).Trim()
|
||||
}
|
||||
}
|
||||
if (!$alias_info) {
|
||||
info 'No alias found.'
|
||||
return
|
||||
}
|
||||
$alias_info = $alias_info | Sort-Object Name
|
||||
$properties = @('Name', 'Command')
|
||||
if ($verbose) {
|
||||
$properties += 'Summary'
|
||||
}
|
||||
$alias_info | Select-Object $properties
|
||||
}
|
||||
|
||||
300
lib/core.ps1
300
lib/core.ps1
@@ -63,18 +63,6 @@ function Optimize-SecurityProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Encoding($wc) {
|
||||
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
|
||||
return [System.Text.Encoding]::GetEncoding($Matches[1])
|
||||
} else {
|
||||
return [System.Text.Encoding]::GetEncoding('utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
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:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
|
||||
}
|
||||
|
||||
function Show-DeprecatedWarning {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -216,34 +204,15 @@ function Complete-ConfigChange {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setup_proxy() {
|
||||
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
|
||||
$proxy = get_config PROXY
|
||||
if(!$proxy) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
$credentials, $address = $proxy -split '(?<!\\)@'
|
||||
if(!$address) {
|
||||
$address, $credentials = $credentials, $null # no credentials supplied
|
||||
if ($Name -eq 'use_sqlite_cache' -and $Value -eq $true) {
|
||||
if ((Get-DefaultArchitecture) -eq 'arm64') {
|
||||
abort 'SQLite cache is not supported on ARM64 platform.'
|
||||
}
|
||||
|
||||
if($address -eq 'none') {
|
||||
[net.webrequest]::defaultwebproxy = $null
|
||||
} elseif($address -ne 'default') {
|
||||
[net.webrequest]::defaultwebproxy = new-object net.webproxy "http://$address"
|
||||
}
|
||||
|
||||
if($credentials -eq 'currentuser') {
|
||||
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
|
||||
} elseif($credentials) {
|
||||
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])','$1' }
|
||||
[net.webrequest]::defaultwebproxy.credentials = new-object net.networkcredential($username, $password)
|
||||
}
|
||||
} catch {
|
||||
warn "Failed to use proxy '$proxy': $($_.exception.message)"
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||
info 'Initializing SQLite cache in progress... This may take a while, please wait.'
|
||||
Set-ScoopDB
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,10 +283,6 @@ function Invoke-GitLog {
|
||||
# helper functions
|
||||
function coalesce($a, $b) { if($a) { return $a } $b }
|
||||
|
||||
function format($str, $hash) {
|
||||
$hash.keys | ForEach-Object { set-variable $_ $hash[$_] }
|
||||
$executionContext.invokeCommand.expandString($str)
|
||||
}
|
||||
function is_admin {
|
||||
$admin = [security.principal.windowsbuiltinrole]::administrator
|
||||
$id = [security.principal.windowsidentity]::getcurrent()
|
||||
@@ -399,7 +364,22 @@ function currentdir($app, $global) {
|
||||
function persistdir($app, $global) { "$(basedir $global)\persist\$app" }
|
||||
function usermanifestsdir { "$(basedir)\workspace" }
|
||||
function usermanifest($app) { "$(usermanifestsdir)\$app.json" }
|
||||
function cache_path($app, $version, $url) { "$cachedir\$app#$version#$($url -replace '[^\w\.\-]+', '_')" }
|
||||
function cache_path($app, $version, $url) {
|
||||
$underscoredUrl = $url -replace '[^\w\.\-]+', '_'
|
||||
$filePath = Join-Path $cachedir "$app#$version#$underscoredUrl"
|
||||
|
||||
# NOTE: Scoop cache files migration. Remove this 6 months after the feature ships.
|
||||
if (Test-Path $filePath) {
|
||||
return $filePath
|
||||
}
|
||||
|
||||
$urlStream = [System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($url))
|
||||
$sha = (Get-FileHash -Algorithm SHA256 -InputStream $urlStream).Hash.ToLower().Substring(0, 7)
|
||||
$extension = [System.IO.Path]::GetExtension($url)
|
||||
$filePath = $filePath -replace "$underscoredUrl", "$sha$extension"
|
||||
|
||||
return $filePath
|
||||
}
|
||||
|
||||
# apps
|
||||
function sanitary_path($path) { return [regex]::replace($path, "[/\\?:*<>|]", "") }
|
||||
@@ -477,7 +457,7 @@ function Get-HelperPath {
|
||||
[OutputType([String])]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
|
||||
[ValidateSet('Git', '7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')]
|
||||
[ValidateSet('Git', '7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2')]
|
||||
[String]
|
||||
$Helper
|
||||
)
|
||||
@@ -491,12 +471,17 @@ function Get-HelperPath {
|
||||
if ($internalgit) {
|
||||
$HelperPath = $internalgit
|
||||
} else {
|
||||
$HelperPath = (Get-Command git -ErrorAction Ignore).Source
|
||||
$HelperPath = (Get-Command git -CommandType Application -TotalCount 1 -ErrorAction Ignore).Source
|
||||
}
|
||||
}
|
||||
'7zip' { $HelperPath = Get-AppFilePath '7zip' '7z.exe' }
|
||||
'Lessmsi' { $HelperPath = Get-AppFilePath 'lessmsi' 'lessmsi.exe' }
|
||||
'Innounp' { $HelperPath = Get-AppFilePath 'innounp' 'innounp.exe' }
|
||||
'Innounp' {
|
||||
$HelperPath = Get-AppFilePath 'innounp-unicode' 'innounp.exe'
|
||||
if ([String]::IsNullOrEmpty($HelperPath)) {
|
||||
$HelperPath = Get-AppFilePath 'innounp' 'innounp.exe'
|
||||
}
|
||||
}
|
||||
'Dark' {
|
||||
$HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe'
|
||||
if ([String]::IsNullOrEmpty($HelperPath)) {
|
||||
@@ -504,7 +489,6 @@ function Get-HelperPath {
|
||||
}
|
||||
}
|
||||
'Aria2' { $HelperPath = Get-AppFilePath 'aria2' 'aria2c.exe' }
|
||||
'Zstd' { $HelperPath = Get-AppFilePath 'zstd' 'zstd.exe' }
|
||||
}
|
||||
|
||||
return $HelperPath
|
||||
@@ -551,7 +535,7 @@ function Test-HelperInstalled {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
|
||||
[ValidateSet('7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')]
|
||||
[ValidateSet('7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2')]
|
||||
[String]
|
||||
$Helper
|
||||
)
|
||||
@@ -559,10 +543,6 @@ function Test-HelperInstalled {
|
||||
return ![String]::IsNullOrWhiteSpace((Get-HelperPath -Helper $Helper))
|
||||
}
|
||||
|
||||
function Test-Aria2Enabled {
|
||||
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
|
||||
}
|
||||
|
||||
function app_status($app, $global) {
|
||||
$status = @{}
|
||||
$status.installed = installed $app $global
|
||||
@@ -574,6 +554,9 @@ function app_status($app, $global) {
|
||||
$status.failed = failed $app $global
|
||||
$status.hold = ($install_info.hold -eq $true)
|
||||
|
||||
$deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
|
||||
$status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
|
||||
|
||||
$manifest = manifest $app $install_info.bucket $install_info.url
|
||||
$status.removed = (!$manifest)
|
||||
if ($manifest.version) {
|
||||
@@ -601,7 +584,6 @@ function app_status($app, $global) {
|
||||
if ($deps) {
|
||||
$status.missing_deps += , $deps
|
||||
}
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
@@ -614,28 +596,6 @@ function fname($path) { split-path $path -leaf }
|
||||
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
|
||||
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
|
||||
function strip_fragment($url) { $url -replace (new-object uri $url).fragment }
|
||||
|
||||
function url_filename($url) {
|
||||
(split-path $url -leaf).split('?') | Select-Object -First 1
|
||||
}
|
||||
# Unlike url_filename which can be tricked by appending a
|
||||
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
|
||||
# this function extracts the original filename from the URL.
|
||||
function url_remote_filename($url) {
|
||||
$uri = (New-Object URI $url)
|
||||
$basename = Split-Path $uri.PathAndQuery -Leaf
|
||||
If ($basename -match ".*[?=]+([\w._-]+)") {
|
||||
$basename = $matches[1]
|
||||
}
|
||||
If (($basename -notlike "*.*") -or ($basename -match "^[v.\d]+$")) {
|
||||
$basename = Split-Path $uri.AbsolutePath -Leaf
|
||||
}
|
||||
If (($basename -notlike "*.*") -and ($uri.Fragment -ne "")) {
|
||||
$basename = $uri.Fragment.Trim('/', '#')
|
||||
}
|
||||
return $basename
|
||||
}
|
||||
|
||||
function ensure($dir) {
|
||||
if (!(Test-Path -Path $dir)) {
|
||||
New-Item -Path $dir -ItemType Directory | Out-Null
|
||||
@@ -746,26 +706,35 @@ function Invoke-ExternalCommand {
|
||||
$Process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
}
|
||||
if ($ArgumentList.Length -gt 0) {
|
||||
$ArgumentList = $ArgumentList | ForEach-Object { [regex]::Split($_.Replace('"', ''), '(?<=(?<![:\w])[/-]\w+) | (?=[/-])') }
|
||||
if ($FilePath -match '^((cmd|cscript|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$') {
|
||||
$Process.StartInfo.Arguments = $ArgumentList -join ' '
|
||||
} elseif ($Process.StartInfo.ArgumentList.Add) {
|
||||
# Remove existing double quotes and split arguments
|
||||
# '(?<=(?<![:\w])[/-]\w+) ' matches a space after a command line switch starting with a slash ('/') or a hyphen ('-')
|
||||
# The inner item '(?<![:\w])[/-]' matches a slash ('/') or a hyphen ('-') not preceded by a colon (':') or a word character ('\w')
|
||||
# so that it must be a command line switch, otherwise, it would be a path (e.g. 'C:/Program Files') or other word (e.g. 'some-arg')
|
||||
# ' (?=[/-])' matches a space followed by a slash ('/') or a hyphen ('-'), i.e. the space before a command line switch
|
||||
$ArgumentList = $ArgumentList.ForEach({ $_ -replace '"' -split '(?<=(?<![:\w])[/-]\w+) | (?=[/-])' })
|
||||
# Use legacy argument escaping for commands having non-standard behavior with regard to argument passing.
|
||||
# `msiexec` requires some args like `TARGETDIR="C:\Program Files"`, which is non-standard, therefore we treat it as a legacy command.
|
||||
# NSIS installer's '/D' param may not work with the ArgumentList property, so we need to escape arguments manually.
|
||||
# ref-1: https://learn.microsoft.com/en-us/powershell/scripting/learn/experimental-features?view=powershell-7.4#psnativecommandargumentpassing
|
||||
# ref-2: https://nsis.sourceforge.io/Docs/Chapter3.html
|
||||
$LegacyCommand = $FilePath -match '^((cmd|cscript|find|sqlcmd|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$' -or
|
||||
($ArgumentList -match '^/S$|^/D=[A-Z]:[\\/].*$').Length -eq 2
|
||||
$SupportArgumentList = $Process.StartInfo.PSObject.Properties.Name -contains 'ArgumentList'
|
||||
if ((-not $LegacyCommand) -and $SupportArgumentList) {
|
||||
# ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+)
|
||||
# ref-1: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0
|
||||
# ref-2: https://docs.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.2#net-framework-vs-net-core
|
||||
$ArgumentList | ForEach-Object { $Process.StartInfo.ArgumentList.Add($_) }
|
||||
$ArgumentList.ForEach({ $Process.StartInfo.ArgumentList.Add($_) })
|
||||
} else {
|
||||
# escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
|
||||
$escapedArgs = $ArgumentList | ForEach-Object {
|
||||
# escape N consecutive backslash(es), which are followed by a double quote or at the end of the string, to 2N consecutive ones
|
||||
$s = $_ -replace '(\\+)(""|$)', '$1$1$2'
|
||||
# quote the path if it contains spaces and is not NSIS's '/D' argument
|
||||
# ref: https://nsis.sourceforge.io/Docs/Chapter3.html
|
||||
if ($s -match ' ' -and $s -notmatch '/D=[A-Z]:[\\/].*') {
|
||||
$s -replace '([A-Z]:[\\/].*)', '"$1"'
|
||||
} else {
|
||||
$s
|
||||
}
|
||||
# Escape arguments manually in lower versions
|
||||
$escapedArgs = switch -regex ($ArgumentList) {
|
||||
# Quote paths starting with a drive letter
|
||||
'(?<!/D=)[A-Z]:[\\/].*' { $_ -replace '([A-Z]:[\\/].*)', '"$1"'; continue }
|
||||
# Do not quote paths if it is NSIS's '/D' argument
|
||||
'/D=[A-Z]:[\\/].*' { $_; continue }
|
||||
# Quote args with spaces
|
||||
' ' { "`"$_`""; continue }
|
||||
default { $_; continue }
|
||||
}
|
||||
$Process.StartInfo.Arguments = $escapedArgs -join ' '
|
||||
}
|
||||
@@ -1004,19 +973,20 @@ function shim($path, $global, $name, $arg) {
|
||||
warn_on_overwrite "$shim.cmd" $path
|
||||
@(
|
||||
"@rem $resolved_path",
|
||||
"@cd /d $(Split-Path $resolved_path -Parent)"
|
||||
"@java -jar `"$resolved_path`" $arg %*"
|
||||
"@pushd $(Split-Path $resolved_path -Parent)",
|
||||
"@java -jar `"$resolved_path`" $arg %*",
|
||||
"@popd"
|
||||
) -join "`r`n" | Out-UTF8File "$shim.cmd"
|
||||
|
||||
warn_on_overwrite $shim $path
|
||||
@(
|
||||
"#!/bin/sh",
|
||||
"# $resolved_path",
|
||||
"if [ `$(echo `$WSL_DISTRO_NAME) ]",
|
||||
"if [ `$WSL_INTEROP ]",
|
||||
'then',
|
||||
" cd `$(wslpath -u '$(Split-Path $resolved_path -Parent)')",
|
||||
'else',
|
||||
" cd `"$((Split-Path $resolved_path -Parent).Replace('\', '/'))`"",
|
||||
" cd `$(cygpath -u '$(Split-Path $resolved_path -Parent)')",
|
||||
'fi',
|
||||
"java.exe -jar `"$resolved_path`" $arg `"$@`""
|
||||
) -join "`n" | Out-UTF8File $shim -NoNewLine
|
||||
@@ -1029,22 +999,37 @@ function shim($path, $global, $name, $arg) {
|
||||
|
||||
warn_on_overwrite $shim $path
|
||||
@(
|
||||
"#!/bin/sh",
|
||||
'#!/bin/sh',
|
||||
"# $resolved_path",
|
||||
"python.exe `"$resolved_path`" $arg `"$@`""
|
||||
) -join "`n" | Out-UTF8File $shim -NoNewLine
|
||||
} else {
|
||||
warn_on_overwrite "$shim.cmd" $path
|
||||
$quoted_arg = if ($arg.Count -gt 0) { $arg | ForEach-Object { "`"$_`"" } }
|
||||
@(
|
||||
"@rem $resolved_path",
|
||||
"@bash `"$resolved_path`" $arg %*"
|
||||
'@echo off',
|
||||
'bash -c "command -v wslpath >/dev/null"',
|
||||
'if %errorlevel% equ 0 (',
|
||||
" bash `"`$(wslpath -u '$resolved_path')`" $quoted_arg %*",
|
||||
') else (',
|
||||
" set args=$quoted_arg %*",
|
||||
' setlocal enabledelayedexpansion',
|
||||
' if not "!args!"=="" set args=!args:"=""!',
|
||||
" bash -c `"`$(cygpath -u '$resolved_path') !args!`"",
|
||||
')'
|
||||
) -join "`r`n" | Out-UTF8File "$shim.cmd"
|
||||
|
||||
warn_on_overwrite $shim $path
|
||||
@(
|
||||
"#!/bin/sh",
|
||||
'#!/bin/sh',
|
||||
"# $resolved_path",
|
||||
"`"$resolved_path`" $arg `"$@`""
|
||||
"if [ `$WSL_INTEROP ]",
|
||||
'then',
|
||||
" `"`$(wslpath -u '$resolved_path')`" $arg `"$@`"",
|
||||
'else',
|
||||
" `"`$(cygpath -u '$resolved_path')`" $arg `"$@`"",
|
||||
'fi'
|
||||
) -join "`n" | Out-UTF8File $shim -NoNewLine
|
||||
}
|
||||
}
|
||||
@@ -1163,7 +1148,7 @@ function applist($apps, $global) {
|
||||
}
|
||||
|
||||
function parse_app([string]$app) {
|
||||
if ($app -match '^(?:(?<bucket>[a-zA-Z0-9-_.]+)/)?(?<app>.*\.json$|[a-zA-Z0-9-_.]+)(?:@(?<version>.*))?$') {
|
||||
if ($app -match '^(?:(?<bucket>[a-zA-Z0-9-_.]+)/)?(?<app>.*\.json|[a-zA-Z0-9-_.]+)(?:@(?<version>.*))?$') {
|
||||
return $Matches['app'], $Matches['bucket'], $Matches['version']
|
||||
} else {
|
||||
return $app, $null, $null
|
||||
@@ -1216,8 +1201,8 @@ function Test-ScoopCoreOnHold() {
|
||||
}
|
||||
|
||||
function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
|
||||
$newentity = $entity
|
||||
if ($null -ne $newentity) {
|
||||
if ($null -ne $entity) {
|
||||
$newentity = $entity.PSObject.Copy()
|
||||
switch ($entity.GetType().Name) {
|
||||
'String' {
|
||||
$params.GetEnumerator() | ForEach-Object {
|
||||
@@ -1229,7 +1214,7 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
|
||||
}
|
||||
}
|
||||
'Object[]' {
|
||||
$newentity = $entity | ForEach-Object { ,(substitute $_ $params $regexEscape) }
|
||||
$newentity = $entity | ForEach-Object { , (substitute $_ $params $regexEscape) }
|
||||
}
|
||||
'PSCustomObject' {
|
||||
$newentity.PSObject.Properties | ForEach-Object { $_.Value = substitute $_.Value $params $regexEscape }
|
||||
@@ -1239,112 +1224,6 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
|
||||
return $newentity
|
||||
}
|
||||
|
||||
function format_hash([String] $hash) {
|
||||
$hash = $hash.toLower()
|
||||
switch ($hash.Length)
|
||||
{
|
||||
32 { $hash = "md5:$hash" } # md5
|
||||
40 { $hash = "sha1:$hash" } # sha1
|
||||
64 { $hash = $hash } # sha256
|
||||
128 { $hash = "sha512:$hash" } # sha512
|
||||
default { $hash = $null }
|
||||
}
|
||||
return $hash
|
||||
}
|
||||
|
||||
function format_hash_aria2([String] $hash) {
|
||||
$hash = $hash -split ':' | Select-Object -Last 1
|
||||
switch ($hash.Length)
|
||||
{
|
||||
32 { $hash = "md5=$hash" } # md5
|
||||
40 { $hash = "sha-1=$hash" } # sha1
|
||||
64 { $hash = "sha-256=$hash" } # sha256
|
||||
128 { $hash = "sha-512=$hash" } # sha512
|
||||
default { $hash = $null }
|
||||
}
|
||||
return $hash
|
||||
}
|
||||
|
||||
function get_hash([String] $multihash) {
|
||||
$type, $hash = $multihash -split ':'
|
||||
if(!$hash) {
|
||||
# no type specified, assume sha256
|
||||
$type, $hash = 'sha256', $multihash
|
||||
}
|
||||
|
||||
if(@('md5','sha1','sha256', 'sha512') -notcontains $type) {
|
||||
return $null, "Hash type '$type' isn't supported."
|
||||
}
|
||||
|
||||
return $type, $hash.ToLower()
|
||||
}
|
||||
|
||||
function Get-GitHubToken {
|
||||
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)
|
||||
{
|
||||
# FossHub.com
|
||||
if ($url -match "^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$") {
|
||||
$Body = @{
|
||||
projectUri = $Matches.name;
|
||||
fileName = $Matches.filename;
|
||||
source = 'CF';
|
||||
isLatestVersion = $true
|
||||
}
|
||||
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
|
||||
$Body.Add("projectId", $Matches.pid)
|
||||
$Body.Add("releaseId", $Matches.rid)
|
||||
}
|
||||
$url = Invoke-RestMethod -Method Post -Uri "https://api.fosshub.com/download/" -ContentType "application/json" -Body (ConvertTo-Json $Body -Compress)
|
||||
if ($null -eq $url.error) {
|
||||
$url = $url.data.url
|
||||
}
|
||||
}
|
||||
|
||||
# Sourceforge.net
|
||||
if ($url -match "(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)") {
|
||||
# Reshapes the URL to avoid redirections
|
||||
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
|
||||
}
|
||||
|
||||
# Github.com
|
||||
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
|
||||
$headers = @{ "Authorization" = "token $token" }
|
||||
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
|
||||
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
|
||||
|
||||
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
|
||||
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
|
||||
}
|
||||
}
|
||||
|
||||
return $url
|
||||
}
|
||||
|
||||
function get_magic_bytes($file) {
|
||||
if(!(Test-Path $file)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
|
||||
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
|
||||
return Get-Content $file -AsByteStream -TotalCount 8
|
||||
}
|
||||
else {
|
||||
return Get-Content $file -Encoding byte -TotalCount 8
|
||||
}
|
||||
}
|
||||
|
||||
function get_magic_bytes_pretty($file, $glue = ' ') {
|
||||
if(!(Test-Path $file)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
|
||||
}
|
||||
|
||||
function Out-UTF8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $True, Position = 0)]
|
||||
@@ -1430,6 +1309,3 @@ $scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
|
||||
|
||||
# OS information
|
||||
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
|
||||
|
||||
# Setup proxy globally
|
||||
setup_proxy
|
||||
|
||||
410
lib/database.ps1
Normal file
410
lib/database.ps1
Normal file
@@ -0,0 +1,410 @@
|
||||
# Description: Functions for interacting with the Scoop database cache
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get SQLite .NET driver
|
||||
.DESCRIPTION
|
||||
Download and extract the SQLite .NET driver from NuGet.
|
||||
.PARAMETER Version
|
||||
System.String
|
||||
The version of the SQLite .NET driver to download.
|
||||
.INPUTS
|
||||
None
|
||||
.OUTPUTS
|
||||
System.Boolean
|
||||
True if the SQLite .NET driver was successfully downloaded and extracted, otherwise false.
|
||||
#>
|
||||
function Get-SQLite {
|
||||
param (
|
||||
[string]$Version = '1.0.118'
|
||||
)
|
||||
# Install SQLite
|
||||
try {
|
||||
Write-Host "Downloading SQLite $Version..." -ForegroundColor DarkYellow
|
||||
$sqlitePkgPath = "$env:TEMP\sqlite.zip"
|
||||
$sqliteTempPath = "$env:TEMP\sqlite"
|
||||
$sqlitePath = "$PSScriptRoot\..\supporting\sqlite"
|
||||
Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/stub.system.data.sqlite.core.netframework/$version/stub.system.data.sqlite.core.netframework.$version.nupkg" -OutFile $sqlitePkgPath
|
||||
Write-Host "Extracting SQLite $Version..." -ForegroundColor DarkYellow -NoNewline
|
||||
Expand-Archive -Path $sqlitePkgPath -DestinationPath $sqliteTempPath -Force
|
||||
New-Item -Path $sqlitePath -ItemType Directory -Force | Out-Null
|
||||
Move-Item -Path "$sqliteTempPath\build\net451\*", "$sqliteTempPath\lib\net451\System.Data.SQLite.dll" -Destination $sqlitePath -Force
|
||||
Remove-Item -Path $sqlitePkgPath, $sqliteTempPath -Recurse -Force
|
||||
Write-Host ' Done' -ForegroundColor DarkYellow
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Open Scoop SQLite database.
|
||||
.DESCRIPTION
|
||||
Open Scoop SQLite database connection and create the necessary tables if not exists.
|
||||
.INPUTS
|
||||
None
|
||||
.OUTPUTS
|
||||
System.Data.SQLite.SQLiteConnection
|
||||
The SQLite database connection if **PassThru** is used.
|
||||
#>
|
||||
function Open-ScoopDB {
|
||||
# Load System.Data.SQLite
|
||||
if (!('System.Data.SQLite.SQLiteConnection' -as [Type])) {
|
||||
try {
|
||||
if (!(Test-Path -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll")) {
|
||||
Get-SQLite | Out-Null
|
||||
}
|
||||
Add-Type -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll"
|
||||
} catch {
|
||||
throw "Scoop's Database cache requires the ADO.NET driver:`n`thttp://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki"
|
||||
}
|
||||
}
|
||||
$dbPath = Join-Path $scoopdir 'scoop.db'
|
||||
$db = New-Object -TypeName System.Data.SQLite.SQLiteConnection
|
||||
$db.ConnectionString = "Data Source=$dbPath"
|
||||
$db.ParseViaFramework = $true # Allow UNC path
|
||||
$db.Open()
|
||||
$tableCommand = $db.CreateCommand()
|
||||
$tableCommand.CommandText = "CREATE TABLE IF NOT EXISTS 'app' (
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
description TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
bucket VARCHAR NOT NULL,
|
||||
manifest JSON NOT NULL,
|
||||
binary TEXT,
|
||||
shortcut TEXT,
|
||||
dependency TEXT,
|
||||
suggest TEXT,
|
||||
PRIMARY KEY (name, version, bucket)
|
||||
)"
|
||||
$tableCommand.CommandType = [System.Data.CommandType]::Text
|
||||
$tableCommand.ExecuteNonQuery() | Out-Null
|
||||
$tableCommand.Dispose()
|
||||
return $db
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Set Scoop database item(s).
|
||||
.DESCRIPTION
|
||||
Insert or replace item(s) into the Scoop SQLite database.
|
||||
.PARAMETER InputObject
|
||||
System.Object[]
|
||||
The database item(s) to insert or replace.
|
||||
.INPUTS
|
||||
System.Object[]
|
||||
.OUTPUTS
|
||||
None
|
||||
#>
|
||||
function Set-ScoopDBItem {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
|
||||
[psobject[]]
|
||||
$InputObject
|
||||
)
|
||||
|
||||
begin {
|
||||
$db = Open-ScoopDB
|
||||
$dbTrans = $db.BeginTransaction()
|
||||
# TODO Support [hashtable]$InputObject
|
||||
$colName = @($InputObject | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)
|
||||
$dbQuery = "INSERT OR REPLACE INTO app ($($colName -join ', ')) VALUES ($('@' + ($colName -join ', @')))"
|
||||
$dbCommand = $db.CreateCommand()
|
||||
$dbCommand.CommandText = $dbQuery
|
||||
$dbCommand.CommandType = [System.Data.CommandType]::Text
|
||||
}
|
||||
process {
|
||||
foreach ($item in $InputObject) {
|
||||
$item.PSObject.Properties | ForEach-Object {
|
||||
$dbCommand.Parameters.AddWithValue("@$($_.Name)", $_.Value) | Out-Null
|
||||
}
|
||||
$dbCommand.ExecuteNonQuery() | Out-Null
|
||||
}
|
||||
}
|
||||
end {
|
||||
try {
|
||||
$dbTrans.Commit()
|
||||
} catch {
|
||||
$dbTrans.Rollback()
|
||||
throw $_
|
||||
} finally {
|
||||
$dbCommand.Dispose()
|
||||
$dbTrans.Dispose()
|
||||
$db.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Set Scoop app database item(s).
|
||||
.DESCRIPTION
|
||||
Insert or replace Scoop app(s) into the database.
|
||||
.PARAMETER Path
|
||||
System.String
|
||||
The path to the bucket.
|
||||
.PARAMETER CommitHash
|
||||
System.String
|
||||
The commit hash to compare with the HEAD.
|
||||
.INPUTS
|
||||
None
|
||||
.OUTPUTS
|
||||
None
|
||||
#>
|
||||
function Set-ScoopDB {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, ValueFromPipeline)]
|
||||
[string[]]
|
||||
$Path
|
||||
)
|
||||
|
||||
begin {
|
||||
$list = [System.Collections.Generic.List[psobject]]::new()
|
||||
$arch = Get-DefaultArchitecture
|
||||
}
|
||||
process {
|
||||
if ($Path.Count -eq 0) {
|
||||
$bucketPath = Get-LocalBucket | ForEach-Object { Find-BucketDirectory $_ }
|
||||
$Path = (Get-ChildItem $bucketPath -Filter '*.json' -Recurse).FullName
|
||||
}
|
||||
$Path | ForEach-Object {
|
||||
$manifestRaw = [System.IO.File]::ReadAllText($_)
|
||||
$manifest = ConvertFrom-Json $manifestRaw -ErrorAction SilentlyContinue
|
||||
if ($null -ne $manifest.version) {
|
||||
$list.Add([pscustomobject]@{
|
||||
name = $($_ -replace '.*[\\/]([^\\/]+)\.json$', '$1')
|
||||
description = if ($manifest.description) { $manifest.description } else { '' }
|
||||
version = $manifest.version
|
||||
bucket = $($_ -replace '.*buckets[\\/]([^\\/]+)(?:[\\/].*)', '$1')
|
||||
manifest = $manifestRaw
|
||||
binary = $(
|
||||
$result = @()
|
||||
@(arch_specific 'bin' $manifest $arch) | ForEach-Object {
|
||||
if ($_ -is [System.Array]) {
|
||||
$result += "$($_[1]).$($_[0].Split('.')[-1])"
|
||||
} else {
|
||||
$result += $_
|
||||
}
|
||||
}
|
||||
$result -replace '.*?([^\\/]+)?(\.(exe|bat|cmd|ps1|jar|py))$', '$1' -join ' | '
|
||||
)
|
||||
shortcut = $(
|
||||
$result = @()
|
||||
@(arch_specific 'shortcuts' $manifest $arch) | ForEach-Object {
|
||||
$result += $_[1]
|
||||
}
|
||||
$result -replace '.*?([^\\/]+$)', '$1' -join ' | '
|
||||
)
|
||||
dependency = $manifest.depends -join ' | '
|
||||
suggest = $(
|
||||
$suggest_output = @()
|
||||
$manifest.suggest.PSObject.Properties | ForEach-Object {
|
||||
$suggest_output += $_.Value -join ' | '
|
||||
}
|
||||
$suggest_output -join ' | '
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
end {
|
||||
if ($list.Count -ne 0) {
|
||||
Set-ScoopDBItem $list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Select Scoop database item(s).
|
||||
.DESCRIPTION
|
||||
Select item(s) from the Scoop SQLite database.
|
||||
The pattern is matched against the name, binaries, and shortcuts columns for apps.
|
||||
.PARAMETER Pattern
|
||||
System.String
|
||||
The pattern to search for. If is an empty string, all items will be returned.
|
||||
.PARAMETER From
|
||||
System.String[]
|
||||
The fields to search from.
|
||||
.INPUTS
|
||||
System.String
|
||||
.OUTPUTS
|
||||
System.Data.DataTable
|
||||
The selected database item(s).
|
||||
#>
|
||||
function Select-ScoopDBItem {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
|
||||
[AllowEmptyString()]
|
||||
[string]
|
||||
$Pattern,
|
||||
[Parameter(Mandatory, Position = 1)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string[]]
|
||||
$From
|
||||
)
|
||||
|
||||
begin {
|
||||
$db = Open-ScoopDB
|
||||
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
|
||||
$result = New-Object System.Data.DataTable
|
||||
$dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')"
|
||||
$dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket"
|
||||
$dbCommand = $db.CreateCommand()
|
||||
$dbCommand.CommandText = $dbQuery
|
||||
$dbCommand.CommandType = [System.Data.CommandType]::Text
|
||||
$dbAdapter.SelectCommand = $dbCommand
|
||||
}
|
||||
process {
|
||||
$dbCommand.Parameters.AddWithValue('@Pattern', $(if ($Pattern -eq '') { '%' } else { '%' + $Pattern + '%' })) | Out-Null
|
||||
[void]$dbAdapter.Fill($result)
|
||||
}
|
||||
end {
|
||||
$resultCopy = $result.Copy()
|
||||
$dbAdapter.Dispose()
|
||||
$db.Dispose()
|
||||
# Use Write-Output to ensure PowerShell properly returns the DataTable
|
||||
Write-Output $resultCopy -NoEnumerate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get Scoop database item.
|
||||
.DESCRIPTION
|
||||
Get item from the Scoop SQLite database.
|
||||
.PARAMETER Name
|
||||
System.String
|
||||
The name of the item to get.
|
||||
.PARAMETER Bucket
|
||||
System.String
|
||||
The bucket of the item to get.
|
||||
.PARAMETER Version
|
||||
System.String
|
||||
The version of the item to get. If not provided, the latest version will be returned.
|
||||
.INPUTS
|
||||
System.String
|
||||
.OUTPUTS
|
||||
System.Data.DataTable
|
||||
The selected database item.
|
||||
#>
|
||||
function Get-ScoopDBItem {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||
[string]
|
||||
$Name,
|
||||
[Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
|
||||
[string]
|
||||
$Bucket,
|
||||
[Parameter(Position = 2, ValueFromPipelineByPropertyName)]
|
||||
[string]
|
||||
$Version
|
||||
)
|
||||
|
||||
begin {
|
||||
$db = Open-ScoopDB
|
||||
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
|
||||
$dbCommand = $db.CreateCommand()
|
||||
$dbCommand.CommandType = [System.Data.CommandType]::Text
|
||||
$dbAdapter.SelectCommand = $dbCommand
|
||||
}
|
||||
|
||||
process {
|
||||
$result = New-Object System.Data.DataTable
|
||||
|
||||
# Build query based on whether version is specified
|
||||
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
|
||||
if ($Version) {
|
||||
$dbQuery += ' AND version = @Version'
|
||||
} else {
|
||||
$dbQuery += ' ORDER BY version DESC LIMIT 1'
|
||||
}
|
||||
|
||||
$dbCommand.CommandText = $dbQuery
|
||||
$dbCommand.Parameters.Clear()
|
||||
|
||||
# Add parameters for this specific query
|
||||
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
|
||||
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
|
||||
if ($Version) {
|
||||
$dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null
|
||||
}
|
||||
|
||||
[void]$dbAdapter.Fill($result)
|
||||
|
||||
# Create a copy of the DataTable to avoid disposal issues
|
||||
$resultCopy = $result.Copy()
|
||||
|
||||
# Use Write-Output to ensure PowerShell properly returns the DataTable
|
||||
Write-Output $resultCopy -NoEnumerate
|
||||
}
|
||||
|
||||
end {
|
||||
# Clean up all resources
|
||||
$dbAdapter.Dispose()
|
||||
$dbCommand.Dispose()
|
||||
$db.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Remove Scoop database item(s).
|
||||
.DESCRIPTION
|
||||
Remove item(s) from the Scoop SQLite database.
|
||||
.PARAMETER Name
|
||||
System.String
|
||||
The name of the item to remove.
|
||||
.PARAMETER Bucket
|
||||
System.String
|
||||
The bucket of the item to remove.
|
||||
.INPUTS
|
||||
System.String
|
||||
.OUTPUTS
|
||||
None
|
||||
#>
|
||||
function Remove-ScoopDBItem {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||
[string]
|
||||
$Name,
|
||||
[Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
|
||||
[string]
|
||||
$Bucket
|
||||
)
|
||||
|
||||
begin {
|
||||
$db = Open-ScoopDB
|
||||
$dbTrans = $db.BeginTransaction()
|
||||
$dbQuery = 'DELETE FROM app WHERE bucket = @Bucket'
|
||||
$dbCommand = $db.CreateCommand()
|
||||
$dbCommand.CommandText = $dbQuery
|
||||
$dbCommand.CommandType = [System.Data.CommandType]::Text
|
||||
}
|
||||
process {
|
||||
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
|
||||
if ($Name) {
|
||||
$dbCommand.CommandText = $dbQuery + ' AND name = @Name'
|
||||
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
|
||||
}
|
||||
$dbCommand.ExecuteNonQuery() | Out-Null
|
||||
}
|
||||
end {
|
||||
try {
|
||||
$dbTrans.Commit()
|
||||
} catch {
|
||||
$dbTrans.Rollback()
|
||||
throw $_
|
||||
} finally {
|
||||
$dbCommand.Dispose()
|
||||
$dbTrans.Dispose()
|
||||
$db.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,67 @@
|
||||
# Description: Functions for decompressing archives or installers
|
||||
|
||||
function Invoke-Extraction {
|
||||
param (
|
||||
[string]
|
||||
$Path,
|
||||
[string[]]
|
||||
$Name,
|
||||
[psobject]
|
||||
$Manifest,
|
||||
[Alias('Arch', 'Architecture')]
|
||||
[string]
|
||||
$ProcessorArchitecture
|
||||
)
|
||||
|
||||
$uri = @(url $Manifest $ProcessorArchitecture)
|
||||
# 'extract_dir' and 'extract_to' are paired
|
||||
$extractDir = @(extract_dir $Manifest $ProcessorArchitecture)
|
||||
$extractTo = @(extract_to $Manifest $ProcessorArchitecture)
|
||||
$extracted = 0
|
||||
|
||||
for ($i = 0; $i -lt $Name.Length; $i++) {
|
||||
# work out extraction method, if applicable
|
||||
$extractFn = $null
|
||||
switch -regex ($Name[$i]) {
|
||||
'\.zip$' {
|
||||
if ((Test-HelperInstalled -Helper 7zip) -or ((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z))) {
|
||||
$extractFn = 'Expand-7zipArchive'
|
||||
} else {
|
||||
$extractFn = 'Expand-ZipArchive'
|
||||
}
|
||||
continue
|
||||
}
|
||||
'\.msi$' {
|
||||
$extractFn = 'Expand-MsiArchive'
|
||||
continue
|
||||
}
|
||||
'\.exe$' {
|
||||
if ($Manifest.innosetup) {
|
||||
$extractFn = 'Expand-InnoArchive'
|
||||
}
|
||||
continue
|
||||
}
|
||||
{ Test-7zipRequirement -Uri $_ } {
|
||||
$extractFn = 'Expand-7zipArchive'
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ($extractFn) {
|
||||
$fnArgs = @{
|
||||
Path = Join-Path $Path $Name[$i]
|
||||
DestinationPath = Join-Path $Path $extractTo[$extracted]
|
||||
ExtractDir = $extractDir[$extracted]
|
||||
}
|
||||
Write-Host 'Extracting ' -NoNewline
|
||||
Write-Host $(url_remote_filename $uri[$i]) -ForegroundColor Cyan -NoNewline
|
||||
Write-Host ' ... ' -NoNewline
|
||||
& $extractFn @fnArgs -Removal
|
||||
Write-Host 'done.' -ForegroundColor Green
|
||||
$extracted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Expand-7zipArchive {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
@@ -59,15 +123,18 @@ function Expand-7zipArchive {
|
||||
}
|
||||
if (!$IsTar -and $ExtractDir) {
|
||||
movedir "$DestinationPath\$ExtractDir" $DestinationPath | Out-Null
|
||||
# Remove temporary directory
|
||||
Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore
|
||||
# Remove temporary directory if it is empty
|
||||
$ExtractDirTopPath = [string] "$DestinationPath\$($ExtractDir -replace '[\\/].*')"
|
||||
if ((Get-ChildItem -Path $ExtractDirTopPath -Force -ErrorAction Ignore).Count -eq 0) {
|
||||
Remove-Item -Path $ExtractDirTopPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
}
|
||||
if (Test-Path $LogPath) {
|
||||
Remove-Item $LogPath -Force
|
||||
}
|
||||
if ($Removal) {
|
||||
if (($Path -replace '.*\.([^\.]*)$', '$1') -eq '001') {
|
||||
# Remove splited 7-zip archive parts
|
||||
# Remove splitted 7-zip archive parts
|
||||
Get-ChildItem "$($Path -replace '\.[^\.]*$', '').???" | Remove-Item -Force
|
||||
} elseif (($Path -replace '.*\.part(\d+)\.rar$', '$1')[-1] -eq '1') {
|
||||
# Remove splitted RAR archive parts
|
||||
@@ -96,37 +163,9 @@ function Expand-ZstdArchive {
|
||||
[Switch]
|
||||
$Removal
|
||||
)
|
||||
$ZstdPath = Get-HelperPath -Helper Zstd
|
||||
$LogPath = Join-Path (Split-Path $Path) 'zstd.log'
|
||||
$DestinationPath = $DestinationPath.TrimEnd('\')
|
||||
ensure $DestinationPath | Out-Null
|
||||
$ArgList = @('-d', $Path, '--output-dir-flat', $DestinationPath, '-f', '-v')
|
||||
|
||||
if ($Switches) {
|
||||
$ArgList += (-split $Switches)
|
||||
}
|
||||
if ($Removal) {
|
||||
# Remove original archive file
|
||||
$ArgList += '--rm'
|
||||
}
|
||||
$Status = Invoke-ExternalCommand $ZstdPath $ArgList -LogPath $LogPath
|
||||
if (!$Status) {
|
||||
abort "Failed to extract files from $Path.`nLog file:`n $(friendly_path $LogPath)`n$(new_issue_msg $app $bucket 'decompress error')"
|
||||
}
|
||||
$IsTar = (strip_ext $Path) -match '\.tar$'
|
||||
if ($IsTar) {
|
||||
# Check for tar
|
||||
$TarFile = Join-Path $DestinationPath (strip_ext (fname $Path))
|
||||
Expand-7zipArchive -Path $TarFile -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Removal
|
||||
}
|
||||
if (!$IsTar -and $ExtractDir) {
|
||||
movedir (Join-Path $DestinationPath $ExtractDir) $DestinationPath | Out-Null
|
||||
# Remove temporary directory
|
||||
Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
if (Test-Path $LogPath) {
|
||||
Remove-Item $LogPath -Force
|
||||
}
|
||||
# TODO: Remove this function after 2024/12/31
|
||||
Show-DeprecatedWarning $MyInvocation 'Expand-7zipArchive'
|
||||
Expand-7zipArchive -Path $Path -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Switches $Switches -Removal:$Removal
|
||||
}
|
||||
|
||||
function Expand-MsiArchive {
|
||||
@@ -156,7 +195,7 @@ function Expand-MsiArchive {
|
||||
$ArgList = @('x', $Path, "$DestinationPath\")
|
||||
} else {
|
||||
$MsiPath = 'msiexec.exe'
|
||||
$ArgList = @('/a', "`"$Path`"", '/qn', "TARGETDIR=`"$DestinationPath\SourceDir`"")
|
||||
$ArgList = @('/a', $Path, '/qn', "TARGETDIR=$DestinationPath\SourceDir")
|
||||
}
|
||||
$LogPath = "$(Split-Path $Path)\msi.log"
|
||||
if ($Switches) {
|
||||
|
||||
@@ -118,11 +118,8 @@ function Get-InstallationHelper {
|
||||
if ($script -like '*Expand-DarkArchive *') {
|
||||
$helper += 'dark'
|
||||
}
|
||||
if ((Test-ZstdRequirement -Uri $url) -or ($script -like '*Expand-ZstdArchive *')) {
|
||||
$helper += 'zstd'
|
||||
}
|
||||
if (!$All) {
|
||||
'7zip', 'lessmsi', 'innounp', 'dark', 'zstd' | ForEach-Object {
|
||||
'7zip', 'lessmsi', 'innounp', 'dark' | ForEach-Object {
|
||||
if (Test-HelperInstalled -Helper $_) {
|
||||
$helper = $helper -ne $_
|
||||
}
|
||||
@@ -144,22 +141,10 @@ function Test-7zipRequirement {
|
||||
$Uri
|
||||
)
|
||||
return ($Uri | Where-Object {
|
||||
$_ -match '\.((gz)|(tar)|(t[abgpx]z2?)|(lzma)|(bz2?)|(7z)|(001)|(rar)|(iso)|(xz)|(lzh)|(nupkg))(\.[^\d.]+)?$'
|
||||
$_ -match '\.(001|7z|bz(ip)?2?|gz|img|iso|lzma|lzh|nupkg|rar|tar|t[abgpx]z2?|t?zst|xz)(\.[^\d.]+)?$'
|
||||
}).Count -gt 0
|
||||
}
|
||||
|
||||
function Test-ZstdRequirement {
|
||||
[CmdletBinding()]
|
||||
[OutputType([Boolean])]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowNull()]
|
||||
[String[]]
|
||||
$Uri
|
||||
)
|
||||
return ($Uri | Where-Object { $_ -match '\.zst$' }).Count -gt 0
|
||||
}
|
||||
|
||||
function Test-LessmsiRequirement {
|
||||
[CmdletBinding()]
|
||||
[OutputType([Boolean])]
|
||||
|
||||
769
lib/download.ps1
Normal file
769
lib/download.ps1
Normal file
@@ -0,0 +1,769 @@
|
||||
# Description: Functions for downloading files
|
||||
|
||||
## Meta downloader
|
||||
|
||||
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.' }
|
||||
|
||||
# can be multiple urls: if there are, then installer should go first to make 'installer.args' section work
|
||||
$urls = @(script:url $manifest $architecture)
|
||||
|
||||
# can be multiple cookies: they will be used for all HTTP requests.
|
||||
$cookies = $manifest.cookie
|
||||
|
||||
# download first
|
||||
if (Test-Aria2Enabled) {
|
||||
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
|
||||
} else {
|
||||
foreach ($url in $urls) {
|
||||
$fname = url_filename $url
|
||||
|
||||
try {
|
||||
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
|
||||
} catch {
|
||||
Write-Host -ForegroundColor DarkRed $_
|
||||
abort "URL $url is not valid"
|
||||
}
|
||||
|
||||
if ($check_hash) {
|
||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
|
||||
if (!$ok) {
|
||||
error $err
|
||||
$cached = cache_path $app $version $url
|
||||
if (Test-Path $cached) {
|
||||
# rm cached file
|
||||
Remove-Item -Force $cached
|
||||
}
|
||||
if ($url.Contains('sourceforge.net')) {
|
||||
Write-Host -ForegroundColor Yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||
}
|
||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $urls.ForEach({ url_filename $_ })
|
||||
}
|
||||
|
||||
## [System.Net] downloader
|
||||
|
||||
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
|
||||
$cached = cache_path $app $version $url
|
||||
|
||||
if (!(Test-Path $cached) -or !$use_cache) {
|
||||
ensure $cachedir | Out-Null
|
||||
Start-Download $url "$cached.download" $cookies
|
||||
Move-Item "$cached.download" $cached -Force
|
||||
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
|
||||
|
||||
if (!($null -eq $to)) {
|
||||
if ($use_cache) {
|
||||
Copy-Item $cached $to
|
||||
} else {
|
||||
Move-Item $cached $to -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Invoke-Download $url $to $cookies $progress
|
||||
} catch {
|
||||
$e = $_.exception
|
||||
if ($e.Response.StatusCode -eq 'Unauthorized') {
|
||||
warn 'Token might be misconfigured.'
|
||||
}
|
||||
if ($e.innerexception) { $e = $e.innerexception }
|
||||
throw $e
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Download ($url, $to, $cookies, $progress) {
|
||||
# download with filesize and progress indicator
|
||||
$reqUrl = ($url -split '#')[0]
|
||||
$wreq = [Net.WebRequest]::Create($reqUrl)
|
||||
if ($wreq -is [Net.HttpWebRequest]) {
|
||||
$wreq.UserAgent = Get-UserAgent
|
||||
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
|
||||
$wreq.Referer = strip_filename $url
|
||||
}
|
||||
if ($url -match 'api\.github\.com/repos') {
|
||||
$wreq.Accept = 'application/octet-stream'
|
||||
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
|
||||
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
|
||||
}
|
||||
if ($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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$wres = $wreq.GetResponse()
|
||||
} catch [System.Net.WebException] {
|
||||
$exc = $_.Exception
|
||||
$handledCodes = @(
|
||||
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
|
||||
[System.Net.HttpStatusCode]::Found, # HTTP 302
|
||||
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
|
||||
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
|
||||
)
|
||||
|
||||
# Only handle redirection codes
|
||||
$redirectRes = $exc.Response
|
||||
if ($handledCodes -notcontains $redirectRes.StatusCode) {
|
||||
throw $exc
|
||||
}
|
||||
|
||||
# Get the new location of the file
|
||||
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
|
||||
throw $exc
|
||||
}
|
||||
|
||||
$newUrl = $redirectRes.Headers['Location']
|
||||
info "Following redirect to $newUrl..."
|
||||
|
||||
# Handle manual file rename
|
||||
if ($url -like '*#/*') {
|
||||
$null, $postfix = $url -split '#/'
|
||||
$newUrl = "$newUrl`#/$postfix"
|
||||
}
|
||||
|
||||
Invoke-Download $newUrl $to $cookies $progress
|
||||
return
|
||||
}
|
||||
|
||||
$total = $wres.ContentLength
|
||||
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
|
||||
$total = ftp_file_size($url)
|
||||
}
|
||||
|
||||
if ($progress -and ($total -gt 0)) {
|
||||
[console]::CursorVisible = $false
|
||||
function Trace-DownloadProgress ($read) {
|
||||
Write-DownloadProgress $read $total $url
|
||||
}
|
||||
} else {
|
||||
Write-Host "Downloading $url ($(filesize $total))..."
|
||||
function Trace-DownloadProgress {
|
||||
#no op
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$s = $wres.getresponsestream()
|
||||
$fs = [io.file]::openwrite($to)
|
||||
$buffer = New-Object byte[] 2048
|
||||
$totalRead = 0
|
||||
$sw = [diagnostics.stopwatch]::StartNew()
|
||||
|
||||
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()
|
||||
Trace-DownloadProgress $totalRead
|
||||
}
|
||||
}
|
||||
$sw.stop()
|
||||
Trace-DownloadProgress $totalRead
|
||||
} finally {
|
||||
if ($progress) {
|
||||
[console]::CursorVisible = $true
|
||||
Write-Host
|
||||
}
|
||||
if ($fs) {
|
||||
$fs.close()
|
||||
}
|
||||
if ($s) {
|
||||
$s.close()
|
||||
}
|
||||
$wres.close()
|
||||
}
|
||||
}
|
||||
|
||||
function Format-DownloadProgress ($url, $read, $total, $console) {
|
||||
$filename = url_remote_filename $url
|
||||
|
||||
# calculate current percentage done
|
||||
$p = [math]::Round($read / $total * 100, 0)
|
||||
|
||||
# pre-generate LHS and RHS of progress string
|
||||
# so we know how much space we have
|
||||
$left = "$filename ($(filesize $total))"
|
||||
$right = [string]::Format('{0,3}%', $p)
|
||||
|
||||
# calculate remaining width for progress bar
|
||||
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
|
||||
|
||||
# calculate how many characters are completed
|
||||
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
|
||||
|
||||
# generate dashes to symbolise completed
|
||||
if ($completed -gt 1) {
|
||||
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
|
||||
}
|
||||
|
||||
# this is why we calculate $completed - 1 above
|
||||
$dashes += switch ($p) {
|
||||
100 { '=' }
|
||||
default { '>' }
|
||||
}
|
||||
|
||||
# the remaining characters are filled with spaces
|
||||
$spaces = switch ($dashes.Length) {
|
||||
$midwidth { [string]::Empty }
|
||||
default {
|
||||
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
|
||||
}
|
||||
}
|
||||
|
||||
"$left [$dashes$spaces] $right"
|
||||
}
|
||||
|
||||
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 = $(Format-DownloadProgress $url 100 $total $console).Length
|
||||
if (($left + $maxOutputLength) -gt $width) {
|
||||
# not enough room to print progress on this line
|
||||
# print on new line
|
||||
Write-Host
|
||||
$left = 0
|
||||
$top = $top + 1
|
||||
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
|
||||
[console]::SetCursorPosition($left, $top)
|
||||
}
|
||||
|
||||
## Aria2 downloader
|
||||
|
||||
function Test-Aria2Enabled {
|
||||
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
|
||||
}
|
||||
|
||||
function aria_exit_code($exitcode) {
|
||||
$codes = @{
|
||||
0 = 'All downloads were successful'
|
||||
1 = 'An unknown error occurred'
|
||||
2 = 'Timeout'
|
||||
3 = 'Resource was not found'
|
||||
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
|
||||
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
|
||||
6 = 'Network problem occurred.'
|
||||
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
|
||||
8 = 'Remote server did not support resume when resume was required to complete download'
|
||||
9 = 'There was not enough disk space available'
|
||||
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
|
||||
11 = 'Aria2 was downloading same file at that moment'
|
||||
12 = 'Aria2 was downloading same info hash torrent at that moment'
|
||||
13 = 'File already existed. See --allow-overwrite option'
|
||||
14 = 'Renaming file failed. See --auto-file-renaming option'
|
||||
15 = 'Aria2 could not open existing file'
|
||||
16 = 'Aria2 could not create new file or truncate existing file'
|
||||
17 = 'File I/O error occurred'
|
||||
18 = 'Aria2 could not create directory'
|
||||
19 = 'Name resolution failed'
|
||||
20 = 'Aria2 could not parse Metalink document'
|
||||
21 = 'FTP command failed'
|
||||
22 = 'HTTP response header was bad or unexpected'
|
||||
23 = 'Too many redirects occurred'
|
||||
24 = 'HTTP authorization failed'
|
||||
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
|
||||
26 = '".torrent" file was corrupted or missing information that aria2 needed'
|
||||
27 = 'Magnet URI was bad'
|
||||
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
|
||||
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
|
||||
30 = 'Aria2 could not parse JSON-RPC request'
|
||||
31 = 'Reserved. Not used'
|
||||
32 = 'Checksum validation failed'
|
||||
}
|
||||
if ($null -eq $codes[$exitcode]) {
|
||||
return 'An unknown error occurred'
|
||||
}
|
||||
return $codes[$exitcode]
|
||||
}
|
||||
|
||||
function get_filename_from_metalink($file) {
|
||||
$bytes = get_magic_bytes_pretty $file ''
|
||||
# check if file starts with '<?xml'
|
||||
if (!($bytes.StartsWith('3c3f786d6c'))) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Add System.Xml for reading metalink files
|
||||
Add-Type -AssemblyName 'System.Xml'
|
||||
$xr = [System.Xml.XmlReader]::Create($file)
|
||||
$filename = $null
|
||||
try {
|
||||
$xr.ReadStartElement('metalink')
|
||||
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
|
||||
$filename = $xr.Value
|
||||
}
|
||||
} catch [System.Xml.XmlException] {
|
||||
return $null
|
||||
} finally {
|
||||
$xr.Close()
|
||||
}
|
||||
|
||||
return $filename
|
||||
}
|
||||
|
||||
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
|
||||
$data = @{}
|
||||
$urls = @(script:url $manifest $architecture)
|
||||
|
||||
# aria2 input file
|
||||
$urlstxt = Join-Path $cachedir "$app.txt"
|
||||
$urlstxt_content = ''
|
||||
$download_finished = $true
|
||||
|
||||
# aria2 options
|
||||
$options = @(
|
||||
"--input-file='$urlstxt'"
|
||||
"--user-agent='$(Get-UserAgent)'"
|
||||
'--allow-overwrite=true'
|
||||
'--auto-file-renaming=false'
|
||||
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
|
||||
"--split=$(get_config 'aria2-split' 5)"
|
||||
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
|
||||
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
|
||||
'--console-log-level=warn'
|
||||
'--enable-color=false'
|
||||
'--no-conf=true'
|
||||
'--follow-metalink=true'
|
||||
'--metalink-preferred-protocol=https'
|
||||
'--min-tls-version=TLSv1.2'
|
||||
"--stop-with-process=$PID"
|
||||
'--continue'
|
||||
'--summary-interval=0'
|
||||
'--auto-save-interval=1'
|
||||
)
|
||||
|
||||
if ($cookies) {
|
||||
$options += "--header='Cookie: $(cookie_header $cookies)'"
|
||||
}
|
||||
|
||||
$proxy = get_config PROXY
|
||||
if ($proxy -ne 'none') {
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Address) {
|
||||
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
|
||||
}
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
|
||||
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
|
||||
}
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
|
||||
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
|
||||
}
|
||||
}
|
||||
|
||||
$more_options = get_config 'aria2-options'
|
||||
if ($more_options) {
|
||||
$options += $more_options
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
$data.$url = @{
|
||||
'target' = Join-Path $dir (url_filename $url)
|
||||
'cachename' = fname (cache_path $app $version $url)
|
||||
'source' = cache_path $app $version $url
|
||||
}
|
||||
|
||||
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
|
||||
Write-Host 'Loading ' -NoNewline
|
||||
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
|
||||
Write-Host ' from cache.'
|
||||
} else {
|
||||
$download_finished = $false
|
||||
# create aria2 input file content
|
||||
try {
|
||||
$try_url = handle_special_urls $url
|
||||
} catch {
|
||||
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
|
||||
warn 'Token might be misconfigured.'
|
||||
}
|
||||
}
|
||||
$urlstxt_content += "$try_url`n"
|
||||
if (!$url.Contains('sourceforge.net')) {
|
||||
$urlstxt_content += " referer=$(strip_filename $url)`n"
|
||||
}
|
||||
$urlstxt_content += " dir=$cachedir`n"
|
||||
$urlstxt_content += " out=$($data.$url.cachename)`n"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not($download_finished)) {
|
||||
# write aria2 input file
|
||||
if ($urlstxt_content -ne '') {
|
||||
ensure $cachedir | Out-Null
|
||||
# Write aria2 input-file with UTF8NoBOM encoding
|
||||
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
|
||||
}
|
||||
|
||||
# build aria2 command
|
||||
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
|
||||
|
||||
# handle aria2 console output
|
||||
Write-Host 'Starting download with aria2 ...'
|
||||
|
||||
# Set console output encoding to UTF8 for non-ASCII characters printing
|
||||
$oriConsoleEncoding = [Console]::OutputEncoding
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
||||
|
||||
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
|
||||
# Skip blank lines
|
||||
if ([String]::IsNullOrWhiteSpace($_)) { return }
|
||||
|
||||
# Prevent potential overlaping of text when one line is shorter
|
||||
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
|
||||
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
|
||||
$color = 'Gray'
|
||||
|
||||
if ($_.StartsWith('(OK):')) {
|
||||
$noNewLine = $true
|
||||
$color = 'Green'
|
||||
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
|
||||
$noNewLine = $true
|
||||
$color = 'Cyan'
|
||||
} elseif ($_.StartsWith('Download Results:')) {
|
||||
$noNewLine = $false
|
||||
}
|
||||
|
||||
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
|
||||
}
|
||||
Write-Host ''
|
||||
|
||||
if ($lastexitcode -gt 0) {
|
||||
warn "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
|
||||
warn $urlstxt_content
|
||||
warn $aria2
|
||||
warn $(new_issue_msg $app $bucket "download via aria2 failed")
|
||||
|
||||
Write-Host "Fallback to default downloader ..."
|
||||
|
||||
try {
|
||||
foreach ($url in $urls) {
|
||||
Invoke-CachedDownload $app $version $url "$($data.$url.target)" $cookies $use_cache
|
||||
}
|
||||
} catch {
|
||||
Write-Host $_ -ForegroundColor DarkRed
|
||||
abort "URL $url is not valid"
|
||||
}
|
||||
}
|
||||
|
||||
# remove aria2 input file when done
|
||||
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
|
||||
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Revert console encoding
|
||||
[Console]::OutputEncoding = $oriConsoleEncoding
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
|
||||
$metalink_filename = get_filename_from_metalink $data.$url.source
|
||||
if ($metalink_filename) {
|
||||
Remove-Item $data.$url.source -Force
|
||||
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
|
||||
}
|
||||
|
||||
# run hash checks
|
||||
if ($check_hash) {
|
||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
|
||||
if (!$ok) {
|
||||
error $err
|
||||
if (Test-Path $data.$url.source) {
|
||||
# rm cached file
|
||||
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($url.Contains('sourceforge.net')) {
|
||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||
}
|
||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||
}
|
||||
}
|
||||
|
||||
# copy or move file to target location
|
||||
if (!(Test-Path $data.$url.source) ) {
|
||||
abort $(new_issue_msg $app $bucket 'cached file not found')
|
||||
}
|
||||
|
||||
if (!($dir -eq $cachedir)) {
|
||||
if ($use_cache) {
|
||||
Copy-Item $data.$url.source $data.$url.target
|
||||
} else {
|
||||
Move-Item $data.$url.source $data.$url.target -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## Helper functions
|
||||
|
||||
### Downloader parameters
|
||||
|
||||
function cookie_header($cookies) {
|
||||
if (!$cookies) { return }
|
||||
|
||||
$vals = $cookies.psobject.properties | ForEach-Object {
|
||||
"$($_.name)=$($_.value)"
|
||||
}
|
||||
|
||||
[string]::join(';', $vals)
|
||||
}
|
||||
|
||||
function Get-Encoding($wc) {
|
||||
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
|
||||
return [System.Text.Encoding]::GetEncoding($Matches[1])
|
||||
} else {
|
||||
return [System.Text.Encoding]::GetEncoding('utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
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:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
|
||||
}
|
||||
|
||||
function setup_proxy() {
|
||||
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
|
||||
$proxy = get_config PROXY
|
||||
if (!$proxy) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
$credentials, $address = $proxy -split '(?<!\\)@'
|
||||
if (!$address) {
|
||||
$address, $credentials = $credentials, $null # no credentials supplied
|
||||
}
|
||||
|
||||
if ($address -eq 'none') {
|
||||
[net.webrequest]::defaultwebproxy = $null
|
||||
} elseif ($address -ne 'default') {
|
||||
[net.webrequest]::defaultwebproxy = New-Object net.webproxy "http://$address"
|
||||
}
|
||||
|
||||
if ($credentials -eq 'currentuser') {
|
||||
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
|
||||
} elseif ($credentials) {
|
||||
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])', '$1' }
|
||||
[net.webrequest]::defaultwebproxy.credentials = New-Object net.networkcredential($username, $password)
|
||||
}
|
||||
} catch {
|
||||
warn "Failed to use proxy '$proxy': $($_.exception.message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitHubToken {
|
||||
return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1
|
||||
}
|
||||
|
||||
function github_ratelimit_reached {
|
||||
$api_link = 'https://api.github.com/rate_limit'
|
||||
$ret = (download_json $api_link).rate.remaining -eq 0
|
||||
if ($ret) {
|
||||
Write-Host "GitHub API rate limit reached.`r`nPlease try again later or configure your API token using 'scoop config gh_token <your token>'."
|
||||
}
|
||||
$ret
|
||||
}
|
||||
|
||||
### URL handling
|
||||
|
||||
function handle_special_urls($url) {
|
||||
# FossHub.com
|
||||
if ($url -match '^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$') {
|
||||
$Body = @{
|
||||
projectUri = $Matches.name
|
||||
fileName = $Matches.filename
|
||||
source = 'CF'
|
||||
isLatestVersion = $true
|
||||
}
|
||||
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
|
||||
$Body.Add('projectId', $Matches.pid)
|
||||
$Body.Add('releaseId', $Matches.rid)
|
||||
}
|
||||
$url = Invoke-RestMethod -Method Post -Uri 'https://api.fosshub.com/download/' -ContentType 'application/json' -Body (ConvertTo-Json $Body -Compress)
|
||||
if ($null -eq $url.error) {
|
||||
$url = $url.data.url
|
||||
}
|
||||
}
|
||||
|
||||
# Sourceforge.net
|
||||
if ($url -match '(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)') {
|
||||
# Reshapes the URL to avoid redirections
|
||||
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
|
||||
}
|
||||
|
||||
# Github.com
|
||||
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
|
||||
$headers = @{ 'Authorization' = "token $token" }
|
||||
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
|
||||
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
|
||||
|
||||
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
|
||||
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
|
||||
}
|
||||
}
|
||||
|
||||
return $url
|
||||
}
|
||||
|
||||
### Remote file information
|
||||
|
||||
function download_json($url) {
|
||||
$githubtoken = Get-GitHubToken
|
||||
$authheader = @{}
|
||||
if ($githubtoken) {
|
||||
$authheader = @{'Authorization' = "token $githubtoken" }
|
||||
}
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
|
||||
$ProgressPreference = 'Continue'
|
||||
$result
|
||||
}
|
||||
|
||||
function get_magic_bytes($file) {
|
||||
if (!(Test-Path $file)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if ((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
|
||||
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
|
||||
return Get-Content $file -AsByteStream -TotalCount 8
|
||||
} else {
|
||||
return Get-Content $file -Encoding byte -TotalCount 8
|
||||
}
|
||||
}
|
||||
|
||||
function get_magic_bytes_pretty($file, $glue = ' ') {
|
||||
if (!(Test-Path $file)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
|
||||
}
|
||||
|
||||
Function Get-RemoteFileSize ($Uri) {
|
||||
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
|
||||
if (!$response.Headers.StatusCode) {
|
||||
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
|
||||
}
|
||||
}
|
||||
|
||||
function ftp_file_size($url) {
|
||||
$request = [net.ftpwebrequest]::create($url)
|
||||
$request.method = [net.webrequestmethods+ftp]::getfilesize
|
||||
$request.getresponse().contentlength
|
||||
}
|
||||
|
||||
function url_filename($url) {
|
||||
(Split-Path $url -Leaf).split('?') | Select-Object -First 1
|
||||
}
|
||||
|
||||
function url_remote_filename($url) {
|
||||
# Unlike url_filename which can be tricked by appending a
|
||||
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
|
||||
# this function extracts the original filename from the URL.
|
||||
$uri = (New-Object URI $url)
|
||||
$basename = Split-Path $uri.PathAndQuery -Leaf
|
||||
If ($basename -match '.*[?=]+([\w._-]+)') {
|
||||
$basename = $matches[1]
|
||||
}
|
||||
If (($basename -notlike '*.*') -or ($basename -match '^[v.\d]+$')) {
|
||||
$basename = Split-Path $uri.AbsolutePath -Leaf
|
||||
}
|
||||
If (($basename -notlike '*.*') -and ($uri.Fragment -ne '')) {
|
||||
$basename = $uri.Fragment.Trim('/', '#')
|
||||
}
|
||||
return $basename
|
||||
}
|
||||
|
||||
### Hash-related functions
|
||||
|
||||
function hash_for_url($manifest, $url, $arch) {
|
||||
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
|
||||
|
||||
if ($hashes.length -eq 0) { return $null }
|
||||
|
||||
$urls = @(script:url $manifest $arch)
|
||||
|
||||
$index = [array]::IndexOf($urls, $url)
|
||||
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
|
||||
|
||||
@($hashes)[$index]
|
||||
}
|
||||
|
||||
function check_hash($file, $hash, $app_name) {
|
||||
# returns (ok, err)
|
||||
if (!$hash) {
|
||||
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
|
||||
return $true, $null
|
||||
}
|
||||
|
||||
Write-Host 'Checking hash of ' -NoNewline
|
||||
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
|
||||
Write-Host ' ... ' -NoNewline
|
||||
$algorithm, $expected = get_hash $hash
|
||||
if ($null -eq $algorithm) {
|
||||
return $false, "Hash type '$algorithm' isn't supported."
|
||||
}
|
||||
|
||||
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
|
||||
$expected = $expected.ToLower()
|
||||
|
||||
if ($actual -ne $expected) {
|
||||
$msg = "Hash check failed!`n"
|
||||
$msg += "App: $app_name`n"
|
||||
$msg += "URL: $url`n"
|
||||
if (Test-Path $file) {
|
||||
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
|
||||
}
|
||||
if ($expected -or $actual) {
|
||||
$msg += "Expected: $expected`n"
|
||||
$msg += "Actual: $actual"
|
||||
}
|
||||
return $false, $msg
|
||||
}
|
||||
Write-Host 'ok.' -f Green
|
||||
return $true, $null
|
||||
}
|
||||
|
||||
function get_hash([String] $multihash) {
|
||||
$type, $hash = $multihash -split ':'
|
||||
if (!$hash) {
|
||||
# no type specified, assume sha256
|
||||
$type, $hash = 'sha256', $multihash
|
||||
}
|
||||
|
||||
if (@('md5', 'sha1', 'sha256', 'sha512') -notcontains $type) {
|
||||
return $null, "Hash type '$type' isn't supported."
|
||||
}
|
||||
|
||||
return $type, $hash.ToLower()
|
||||
}
|
||||
|
||||
# Setup proxy globally
|
||||
setup_proxy
|
||||
789
lib/install.ps1
789
lib/install.ps1
@@ -50,22 +50,23 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
|
||||
$persist_dir = persistdir $app $global
|
||||
|
||||
$fname = Invoke-ScoopDownload $app $version $manifest $bucket $architecture $dir $use_cache $check_hash
|
||||
Invoke-HookScript -HookType 'pre_install' -Manifest $manifest -Arch $architecture
|
||||
Invoke-Extraction -Path $dir -Name $fname -Manifest $manifest -ProcessorArchitecture $architecture
|
||||
Invoke-HookScript -HookType 'pre_install' -Manifest $manifest -ProcessorArchitecture $architecture
|
||||
|
||||
run_installer $fname $manifest $architecture $dir $global
|
||||
Invoke-Installer -Path $dir -Name $fname -Manifest $manifest -ProcessorArchitecture $architecture -AppName $app -Global:$global
|
||||
ensure_install_dir_not_in_path $dir $global
|
||||
$dir = link_current $dir
|
||||
create_shims $manifest $dir $global $architecture
|
||||
create_startmenu_shortcuts $manifest $dir $global $architecture
|
||||
install_psmodule $manifest $dir $global
|
||||
env_add_path $manifest $dir $global $architecture
|
||||
env_set $manifest $dir $global $architecture
|
||||
env_set $manifest $global $architecture
|
||||
|
||||
# persist data
|
||||
persist_data $manifest $original_dir $persist_dir
|
||||
persist_permission $manifest $global
|
||||
|
||||
Invoke-HookScript -HookType 'post_install' -Manifest $manifest -Arch $architecture
|
||||
Invoke-HookScript -HookType 'post_install' -Manifest $manifest -ProcessorArchitecture $architecture
|
||||
|
||||
# save info for uninstall
|
||||
save_installed_manifest $app $bucket $dir $url
|
||||
@@ -80,687 +81,94 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
|
||||
show_notes $manifest $dir $original_dir $persist_dir
|
||||
}
|
||||
|
||||
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
|
||||
$cached = cache_path $app $version $url
|
||||
|
||||
if (!(Test-Path $cached) -or !$use_cache) {
|
||||
ensure $cachedir | Out-Null
|
||||
Start-Download $url "$cached.download" $cookies
|
||||
Move-Item "$cached.download" $cached -Force
|
||||
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
|
||||
|
||||
if (!($null -eq $to)) {
|
||||
if ($use_cache) {
|
||||
Copy-Item $cached $to
|
||||
} else {
|
||||
Move-Item $cached $to -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Invoke-Download $url $to $cookies $progress
|
||||
} catch {
|
||||
$e = $_.exception
|
||||
if ($e.Response.StatusCode -eq 'Unauthorized') {
|
||||
warn 'Token might be misconfigured.'
|
||||
}
|
||||
if ($e.innerexception) { $e = $e.innerexception }
|
||||
throw $e
|
||||
}
|
||||
}
|
||||
|
||||
function aria_exit_code($exitcode) {
|
||||
$codes = @{
|
||||
0 = 'All downloads were successful'
|
||||
1 = 'An unknown error occurred'
|
||||
2 = 'Timeout'
|
||||
3 = 'Resource was not found'
|
||||
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
|
||||
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
|
||||
6 = 'Network problem occurred.'
|
||||
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
|
||||
8 = 'Remote server did not support resume when resume was required to complete download'
|
||||
9 = 'There was not enough disk space available'
|
||||
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
|
||||
11 = 'Aria2 was downloading same file at that moment'
|
||||
12 = 'Aria2 was downloading same info hash torrent at that moment'
|
||||
13 = 'File already existed. See --allow-overwrite option'
|
||||
14 = 'Renaming file failed. See --auto-file-renaming option'
|
||||
15 = 'Aria2 could not open existing file'
|
||||
16 = 'Aria2 could not create new file or truncate existing file'
|
||||
17 = 'File I/O error occurred'
|
||||
18 = 'Aria2 could not create directory'
|
||||
19 = 'Name resolution failed'
|
||||
20 = 'Aria2 could not parse Metalink document'
|
||||
21 = 'FTP command failed'
|
||||
22 = 'HTTP response header was bad or unexpected'
|
||||
23 = 'Too many redirects occurred'
|
||||
24 = 'HTTP authorization failed'
|
||||
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
|
||||
26 = '".torrent" file was corrupted or missing information that aria2 needed'
|
||||
27 = 'Magnet URI was bad'
|
||||
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
|
||||
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
|
||||
30 = 'Aria2 could not parse JSON-RPC request'
|
||||
31 = 'Reserved. Not used'
|
||||
32 = 'Checksum validation failed'
|
||||
}
|
||||
if ($null -eq $codes[$exitcode]) {
|
||||
return 'An unknown error occurred'
|
||||
}
|
||||
return $codes[$exitcode]
|
||||
}
|
||||
|
||||
function get_filename_from_metalink($file) {
|
||||
$bytes = get_magic_bytes_pretty $file ''
|
||||
# check if file starts with '<?xml'
|
||||
if (!($bytes.StartsWith('3c3f786d6c'))) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Add System.Xml for reading metalink files
|
||||
Add-Type -AssemblyName 'System.Xml'
|
||||
$xr = [System.Xml.XmlReader]::Create($file)
|
||||
$filename = $null
|
||||
try {
|
||||
$xr.ReadStartElement('metalink')
|
||||
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
|
||||
$filename = $xr.Value
|
||||
}
|
||||
} catch [System.Xml.XmlException] {
|
||||
return $null
|
||||
} finally {
|
||||
$xr.Close()
|
||||
}
|
||||
|
||||
return $filename
|
||||
}
|
||||
|
||||
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
|
||||
$data = @{}
|
||||
$urls = @(script:url $manifest $architecture)
|
||||
|
||||
# aria2 input file
|
||||
$urlstxt = Join-Path $cachedir "$app.txt"
|
||||
$urlstxt_content = ''
|
||||
$download_finished = $true
|
||||
|
||||
# aria2 options
|
||||
$options = @(
|
||||
"--input-file='$urlstxt'"
|
||||
"--user-agent='$(Get-UserAgent)'"
|
||||
'--allow-overwrite=true'
|
||||
'--auto-file-renaming=false'
|
||||
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
|
||||
"--split=$(get_config 'aria2-split' 5)"
|
||||
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
|
||||
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
|
||||
'--console-log-level=warn'
|
||||
'--enable-color=false'
|
||||
'--no-conf=true'
|
||||
'--follow-metalink=true'
|
||||
'--metalink-preferred-protocol=https'
|
||||
'--min-tls-version=TLSv1.2'
|
||||
"--stop-with-process=$PID"
|
||||
'--continue'
|
||||
'--summary-interval=0'
|
||||
'--auto-save-interval=1'
|
||||
)
|
||||
|
||||
if ($cookies) {
|
||||
$options += "--header='Cookie: $(cookie_header $cookies)'"
|
||||
}
|
||||
|
||||
$proxy = get_config PROXY
|
||||
if ($proxy -ne 'none') {
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Address) {
|
||||
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
|
||||
}
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
|
||||
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
|
||||
}
|
||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
|
||||
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
|
||||
}
|
||||
}
|
||||
|
||||
$more_options = get_config 'aria2-options'
|
||||
if ($more_options) {
|
||||
$options += $more_options
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
$data.$url = @{
|
||||
'target' = "$dir\$(url_filename $url)"
|
||||
'cachename' = fname (cache_path $app $version $url)
|
||||
'source' = cache_path $app $version $url
|
||||
}
|
||||
|
||||
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
|
||||
Write-Host 'Loading ' -NoNewline
|
||||
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
|
||||
Write-Host ' from cache.'
|
||||
} else {
|
||||
$download_finished = $false
|
||||
# create aria2 input file content
|
||||
try {
|
||||
$try_url = handle_special_urls $url
|
||||
} catch {
|
||||
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
|
||||
warn 'Token might be misconfigured.'
|
||||
}
|
||||
}
|
||||
$urlstxt_content += "$try_url`n"
|
||||
if (!$url.Contains('sourceforge.net')) {
|
||||
$urlstxt_content += " referer=$(strip_filename $url)`n"
|
||||
}
|
||||
$urlstxt_content += " dir=$cachedir`n"
|
||||
$urlstxt_content += " out=$($data.$url.cachename)`n"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not($download_finished)) {
|
||||
# write aria2 input file
|
||||
if ($urlstxt_content -ne '') {
|
||||
ensure $cachedir | Out-Null
|
||||
# Write aria2 input-file with UTF8NoBOM encoding
|
||||
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
|
||||
}
|
||||
|
||||
# build aria2 command
|
||||
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
|
||||
|
||||
# handle aria2 console output
|
||||
Write-Host 'Starting download with aria2 ...'
|
||||
|
||||
# Set console output encoding to UTF8 for non-ASCII characters printing
|
||||
$oriConsoleEncoding = [Console]::OutputEncoding
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
||||
|
||||
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
|
||||
# Skip blank lines
|
||||
if ([String]::IsNullOrWhiteSpace($_)) { return }
|
||||
|
||||
# Prevent potential overlaping of text when one line is shorter
|
||||
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
|
||||
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
|
||||
$color = 'Gray'
|
||||
|
||||
if ($_.StartsWith('(OK):')) {
|
||||
$noNewLine = $true
|
||||
$color = 'Green'
|
||||
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
|
||||
$noNewLine = $true
|
||||
$color = 'Cyan'
|
||||
} elseif ($_.StartsWith('Download Results:')) {
|
||||
$noNewLine = $false
|
||||
}
|
||||
|
||||
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
|
||||
}
|
||||
Write-Host ''
|
||||
|
||||
if ($lastexitcode -gt 0) {
|
||||
error "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
|
||||
error $urlstxt_content
|
||||
error $aria2
|
||||
abort $(new_issue_msg $app $bucket 'download via aria2 failed')
|
||||
}
|
||||
|
||||
# remove aria2 input file when done
|
||||
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
|
||||
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Revert console encoding
|
||||
[Console]::OutputEncoding = $oriConsoleEncoding
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
|
||||
$metalink_filename = get_filename_from_metalink $data.$url.source
|
||||
if ($metalink_filename) {
|
||||
Remove-Item $data.$url.source -Force
|
||||
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
|
||||
}
|
||||
|
||||
# run hash checks
|
||||
if ($check_hash) {
|
||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
|
||||
if (!$ok) {
|
||||
error $err
|
||||
if (Test-Path $data.$url.source) {
|
||||
# rm cached file
|
||||
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($url.Contains('sourceforge.net')) {
|
||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||
}
|
||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||
}
|
||||
}
|
||||
|
||||
# copy or move file to target location
|
||||
if (!(Test-Path $data.$url.source) ) {
|
||||
abort $(new_issue_msg $app $bucket 'cached file not found')
|
||||
}
|
||||
|
||||
if (!($dir -eq $cachedir)) {
|
||||
if ($use_cache) {
|
||||
Copy-Item $data.$url.source $data.$url.target
|
||||
} else {
|
||||
Move-Item $data.$url.source $data.$url.target -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# download with filesize and progress indicator
|
||||
function Invoke-Download ($url, $to, $cookies, $progress) {
|
||||
$reqUrl = ($url -split '#')[0]
|
||||
$wreq = [Net.WebRequest]::Create($reqUrl)
|
||||
if ($wreq -is [Net.HttpWebRequest]) {
|
||||
$wreq.UserAgent = Get-UserAgent
|
||||
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
|
||||
$wreq.Referer = strip_filename $url
|
||||
}
|
||||
if ($url -match 'api\.github\.com/repos') {
|
||||
$wreq.Accept = 'application/octet-stream'
|
||||
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
|
||||
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
|
||||
}
|
||||
if ($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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$wres = $wreq.GetResponse()
|
||||
} catch [System.Net.WebException] {
|
||||
$exc = $_.Exception
|
||||
$handledCodes = @(
|
||||
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
|
||||
[System.Net.HttpStatusCode]::Found, # HTTP 302
|
||||
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
|
||||
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
|
||||
)
|
||||
|
||||
# Only handle redirection codes
|
||||
$redirectRes = $exc.Response
|
||||
if ($handledCodes -notcontains $redirectRes.StatusCode) {
|
||||
throw $exc
|
||||
}
|
||||
|
||||
# Get the new location of the file
|
||||
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
|
||||
throw $exc
|
||||
}
|
||||
|
||||
$newUrl = $redirectRes.Headers['Location']
|
||||
info "Following redirect to $newUrl..."
|
||||
|
||||
# Handle manual file rename
|
||||
if ($url -like '*#/*') {
|
||||
$null, $postfix = $url -split '#/'
|
||||
$newUrl = "$newUrl#/$postfix"
|
||||
}
|
||||
|
||||
Invoke-Download $newUrl $to $cookies $progress
|
||||
return
|
||||
}
|
||||
|
||||
$total = $wres.ContentLength
|
||||
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
|
||||
$total = ftp_file_size($url)
|
||||
}
|
||||
|
||||
if ($progress -and ($total -gt 0)) {
|
||||
[console]::CursorVisible = $false
|
||||
function Trace-DownloadProgress ($read) {
|
||||
Write-DownloadProgress $read $total $url
|
||||
}
|
||||
} else {
|
||||
Write-Host "Downloading $url ($(filesize $total))..."
|
||||
function Trace-DownloadProgress {
|
||||
#no op
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$s = $wres.getresponsestream()
|
||||
$fs = [io.file]::openwrite($to)
|
||||
$buffer = New-Object byte[] 2048
|
||||
$totalRead = 0
|
||||
$sw = [diagnostics.stopwatch]::StartNew()
|
||||
|
||||
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()
|
||||
Trace-DownloadProgress $totalRead
|
||||
}
|
||||
}
|
||||
$sw.stop()
|
||||
Trace-DownloadProgress $totalRead
|
||||
} finally {
|
||||
if ($progress) {
|
||||
[console]::CursorVisible = $true
|
||||
Write-Host
|
||||
}
|
||||
if ($fs) {
|
||||
$fs.close()
|
||||
}
|
||||
if ($s) {
|
||||
$s.close()
|
||||
}
|
||||
$wres.close()
|
||||
}
|
||||
}
|
||||
|
||||
function Format-DownloadProgress ($url, $read, $total, $console) {
|
||||
$filename = url_remote_filename $url
|
||||
|
||||
# calculate current percentage done
|
||||
$p = [math]::Round($read / $total * 100, 0)
|
||||
|
||||
# pre-generate LHS and RHS of progress string
|
||||
# so we know how much space we have
|
||||
$left = "$filename ($(filesize $total))"
|
||||
$right = [string]::Format('{0,3}%', $p)
|
||||
|
||||
# calculate remaining width for progress bar
|
||||
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
|
||||
|
||||
# calculate how many characters are completed
|
||||
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
|
||||
|
||||
# generate dashes to symbolise completed
|
||||
if ($completed -gt 1) {
|
||||
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
|
||||
}
|
||||
|
||||
# this is why we calculate $completed - 1 above
|
||||
$dashes += switch ($p) {
|
||||
100 { '=' }
|
||||
default { '>' }
|
||||
}
|
||||
|
||||
# the remaining characters are filled with spaces
|
||||
$spaces = switch ($dashes.Length) {
|
||||
$midwidth { [string]::Empty }
|
||||
default {
|
||||
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
|
||||
}
|
||||
}
|
||||
|
||||
"$left [$dashes$spaces] $right"
|
||||
}
|
||||
|
||||
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 = $(Format-DownloadProgress $url 100 $total $console).length
|
||||
if (($left + $maxOutputLength) -gt $width) {
|
||||
# not enough room to print progress on this line
|
||||
# print on new line
|
||||
Write-Host
|
||||
$left = 0
|
||||
$top = $top + 1
|
||||
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
|
||||
[console]::SetCursorPosition($left, $top)
|
||||
}
|
||||
|
||||
function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) {
|
||||
# we only want to show this warning once
|
||||
if (!$use_cache) { warn 'Cache is being ignored.' }
|
||||
|
||||
# can be multiple urls: if there are, then installer should go last,
|
||||
# so that $fname is set properly
|
||||
$urls = @(script:url $manifest $architecture)
|
||||
|
||||
# can be multiple cookies: they will be used for all HTTP requests.
|
||||
$cookies = $manifest.cookie
|
||||
|
||||
$fname = $null
|
||||
|
||||
# extract_dir and extract_to in manifest are like queues: for each url that
|
||||
# needs to be extracted, will get the next dir from the queue
|
||||
$extract_dirs = @(extract_dir $manifest $architecture)
|
||||
$extract_tos = @(extract_to $manifest $architecture)
|
||||
$extracted = 0
|
||||
|
||||
# download first
|
||||
if (Test-Aria2Enabled) {
|
||||
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
|
||||
} else {
|
||||
foreach ($url in $urls) {
|
||||
$fname = url_filename $url
|
||||
|
||||
try {
|
||||
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
|
||||
} catch {
|
||||
Write-Host -f darkred $_
|
||||
abort "URL $url is not valid"
|
||||
}
|
||||
|
||||
if ($check_hash) {
|
||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
|
||||
if (!$ok) {
|
||||
error $err
|
||||
$cached = cache_path $app $version $url
|
||||
if (Test-Path $cached) {
|
||||
# rm cached file
|
||||
Remove-Item -Force $cached
|
||||
}
|
||||
if ($url.Contains('sourceforge.net')) {
|
||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||
}
|
||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
$fname = url_filename $url
|
||||
|
||||
$extract_dir = $extract_dirs[$extracted]
|
||||
$extract_to = $extract_tos[$extracted]
|
||||
|
||||
# work out extraction method, if applicable
|
||||
$extract_fn = $null
|
||||
if ($manifest.innosetup) {
|
||||
$extract_fn = 'Expand-InnoArchive'
|
||||
} elseif ($fname -match '\.zip$') {
|
||||
# Use 7zip when available (more fast)
|
||||
if (((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z)) -or (Test-HelperInstalled -Helper 7zip)) {
|
||||
$extract_fn = 'Expand-7zipArchive'
|
||||
} else {
|
||||
$extract_fn = 'Expand-ZipArchive'
|
||||
}
|
||||
} elseif ($fname -match '\.msi$') {
|
||||
$extract_fn = 'Expand-MsiArchive'
|
||||
} elseif (Test-ZstdRequirement -Uri $fname) {
|
||||
# Zstd first
|
||||
$extract_fn = 'Expand-ZstdArchive'
|
||||
} elseif (Test-7zipRequirement -Uri $fname) {
|
||||
# 7zip
|
||||
$extract_fn = 'Expand-7zipArchive'
|
||||
}
|
||||
|
||||
if ($extract_fn) {
|
||||
Write-Host 'Extracting ' -NoNewline
|
||||
Write-Host $fname -f Cyan -NoNewline
|
||||
Write-Host ' ... ' -NoNewline
|
||||
& $extract_fn -Path "$dir\$fname" -DestinationPath "$dir\$extract_to" -ExtractDir $extract_dir -Removal
|
||||
Write-Host 'done.' -f Green
|
||||
$extracted++
|
||||
}
|
||||
}
|
||||
|
||||
$fname # returns the last downloaded file
|
||||
}
|
||||
|
||||
function cookie_header($cookies) {
|
||||
if (!$cookies) { return }
|
||||
|
||||
$vals = $cookies.psobject.properties | ForEach-Object {
|
||||
"$($_.name)=$($_.value)"
|
||||
}
|
||||
|
||||
[string]::join(';', $vals)
|
||||
}
|
||||
|
||||
function is_in_dir($dir, $check) {
|
||||
$check -match "^$([regex]::Escape("$dir"))([/\\]|$)"
|
||||
}
|
||||
|
||||
function ftp_file_size($url) {
|
||||
$request = [net.ftpwebrequest]::create($url)
|
||||
$request.method = [net.webrequestmethods+ftp]::getfilesize
|
||||
$request.getresponse().contentlength
|
||||
}
|
||||
|
||||
# hashes
|
||||
function hash_for_url($manifest, $url, $arch) {
|
||||
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
|
||||
|
||||
if ($hashes.length -eq 0) { return $null }
|
||||
|
||||
$urls = @(script:url $manifest $arch)
|
||||
|
||||
$index = [array]::indexof($urls, $url)
|
||||
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
|
||||
|
||||
@($hashes)[$index]
|
||||
}
|
||||
|
||||
# returns (ok, err)
|
||||
function check_hash($file, $hash, $app_name) {
|
||||
if (!$hash) {
|
||||
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
|
||||
return $true, $null
|
||||
}
|
||||
|
||||
Write-Host 'Checking hash of ' -NoNewline
|
||||
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
|
||||
Write-Host ' ... ' -NoNewline
|
||||
$algorithm, $expected = get_hash $hash
|
||||
if ($null -eq $algorithm) {
|
||||
return $false, "Hash type '$algorithm' isn't supported."
|
||||
}
|
||||
|
||||
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
|
||||
$expected = $expected.ToLower()
|
||||
|
||||
if ($actual -ne $expected) {
|
||||
$msg = "Hash check failed!`n"
|
||||
$msg += "App: $app_name`n"
|
||||
$msg += "URL: $url`n"
|
||||
if (Test-Path $file) {
|
||||
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
|
||||
function Invoke-Installer {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[string]
|
||||
$Path,
|
||||
[string[]]
|
||||
$Name,
|
||||
[psobject]
|
||||
$Manifest,
|
||||
[Alias('Arch', 'Architecture')]
|
||||
[ValidateSet('32bit', '64bit', 'arm64')]
|
||||
[string]
|
||||
$ProcessorArchitecture,
|
||||
[string]
|
||||
$AppName,
|
||||
[switch]
|
||||
$Global,
|
||||
[switch]
|
||||
$Uninstall
|
||||
)
|
||||
$type = if ($Uninstall) { 'uninstaller' } else { 'installer' }
|
||||
$installer = arch_specific $type $Manifest $ProcessorArchitecture
|
||||
if ($installer.file -or $installer.args) {
|
||||
# Installer filename is either explicit defined ('installer.file') or file name in the first URL
|
||||
if (!$Name) {
|
||||
$Name = url_filename @(url $manifest $architecture)
|
||||
}
|
||||
if ($expected -or $actual) {
|
||||
$msg += "Expected: $expected`n"
|
||||
$msg += "Actual: $actual"
|
||||
$progName = "$Path\$(coalesce $installer.file $Name[0])"
|
||||
if (!(is_in_dir $Path $progName)) {
|
||||
abort "Error in manifest: $((Get-Culture).TextInfo.ToTitleCase($type)) $progName is outside the app directory."
|
||||
} elseif (!(Test-Path $progName)) {
|
||||
abort "$((Get-Culture).TextInfo.ToTitleCase($type)) $progName is missing."
|
||||
}
|
||||
return $false, $msg
|
||||
}
|
||||
Write-Host 'ok.' -f Green
|
||||
return $true, $null
|
||||
}
|
||||
|
||||
# for dealing with installers
|
||||
function args($config, $dir, $global) {
|
||||
if ($config) { return $config | ForEach-Object { (format $_ @{'dir' = $dir; 'global' = $global }) } }
|
||||
@()
|
||||
}
|
||||
|
||||
function run_installer($fname, $manifest, $architecture, $dir, $global) {
|
||||
$installer = installer $manifest $architecture
|
||||
if ($installer.script) {
|
||||
Write-Output 'Running installer script...'
|
||||
Invoke-Command ([scriptblock]::Create($installer.script -join "`r`n"))
|
||||
return
|
||||
}
|
||||
if ($installer) {
|
||||
$prog = "$dir\$(coalesce $installer.file "$fname")"
|
||||
if (!(is_in_dir $dir $prog)) {
|
||||
abort "Error in manifest: Installer $prog is outside the app directory."
|
||||
$substitutions = @{
|
||||
'$dir' = $Path
|
||||
'$global' = $Global
|
||||
'$version' = $Manifest.version
|
||||
}
|
||||
$arg = @(args $installer.args $dir $global)
|
||||
if ($prog.endswith('.ps1')) {
|
||||
& $prog @arg
|
||||
$fnArgs = substitute $installer.args $substitutions
|
||||
if ($progName.EndsWith('.ps1')) {
|
||||
& $progName @fnArgs
|
||||
} else {
|
||||
$installed = Invoke-ExternalCommand $prog $arg -Activity 'Running installer...'
|
||||
if (!$installed) {
|
||||
abort "Installation aborted. You might need to run 'scoop uninstall $app' before trying again."
|
||||
}
|
||||
# Don't remove installer if "keep" flag is set to true
|
||||
if (!($installer.keep -eq 'true')) {
|
||||
Remove-Item $prog
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run_uninstaller($manifest, $architecture, $dir) {
|
||||
$uninstaller = uninstaller $manifest $architecture
|
||||
$version = $manifest.version
|
||||
if ($uninstaller.script) {
|
||||
Write-Output 'Running uninstaller script...'
|
||||
Invoke-Command ([scriptblock]::Create($uninstaller.script -join "`r`n"))
|
||||
return
|
||||
}
|
||||
|
||||
if ($uninstaller.file) {
|
||||
$prog = "$dir\$($uninstaller.file)"
|
||||
$arg = args $uninstaller.args
|
||||
if (!(is_in_dir $dir $prog)) {
|
||||
warn "Error in manifest: Installer $prog is outside the app directory, skipping."
|
||||
$prog = $null
|
||||
} elseif (!(Test-Path $prog)) {
|
||||
warn "Uninstaller $prog is missing, skipping."
|
||||
$prog = $null
|
||||
}
|
||||
|
||||
if ($prog) {
|
||||
if ($prog.endswith('.ps1')) {
|
||||
& $prog @arg
|
||||
} else {
|
||||
$uninstalled = Invoke-ExternalCommand $prog $arg -Activity 'Running uninstaller...'
|
||||
if (!$uninstalled) {
|
||||
$status = Invoke-ExternalCommand $progName -ArgumentList $fnArgs -Activity "Running $type ..."
|
||||
if (!$status) {
|
||||
if ($Uninstall) {
|
||||
abort 'Uninstallation aborted.'
|
||||
} else {
|
||||
abort "Installation aborted. You might need to run 'scoop uninstall $AppName' before trying again."
|
||||
}
|
||||
}
|
||||
# Don't remove installer if "keep" flag is set to true
|
||||
if (!$installer.keep) {
|
||||
Remove-Item $progName
|
||||
}
|
||||
}
|
||||
}
|
||||
Invoke-HookScript -HookType $type -Manifest $Manifest -ProcessorArchitecture $ProcessorArchitecture
|
||||
}
|
||||
|
||||
function Invoke-HookScript {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('installer', 'pre_install', 'post_install', 'uninstaller', 'pre_uninstall', 'post_uninstall')]
|
||||
[String] $HookType,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[PSCustomObject] $Manifest,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Alias('Arch', 'Architecture')]
|
||||
[ValidateSet('32bit', '64bit', 'arm64')]
|
||||
[string]
|
||||
$ProcessorArchitecture
|
||||
)
|
||||
|
||||
$script = arch_specific $HookType $Manifest $ProcessorArchitecture
|
||||
if ($HookType -in @('installer', 'uninstaller')) {
|
||||
$script = $script.script
|
||||
}
|
||||
if ($script) {
|
||||
Write-Host "Running $HookType script..." -NoNewline
|
||||
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
|
||||
Write-Host 'done.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# get target, name, arguments for shim
|
||||
@@ -924,12 +332,12 @@ function env_rm_path($manifest, $dir, $global, $arch) {
|
||||
}
|
||||
}
|
||||
|
||||
function env_set($manifest, $dir, $global, $arch) {
|
||||
function env_set($manifest, $global, $arch) {
|
||||
$env_set = arch_specific 'env_set' $manifest $arch
|
||||
if ($env_set) {
|
||||
$env_set | Get-Member -Member NoteProperty | ForEach-Object {
|
||||
$name = $_.name
|
||||
$val = format $env_set.$($_.name) @{ 'dir' = $dir }
|
||||
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$val = $ExecutionContext.InvokeCommand.ExpandString($env_set.$($name))
|
||||
Set-EnvVar -Name $name -Value $val -Global:$global
|
||||
Set-Content env:\$name $val
|
||||
}
|
||||
@@ -938,41 +346,20 @@ function env_set($manifest, $dir, $global, $arch) {
|
||||
function env_rm($manifest, $global, $arch) {
|
||||
$env_set = arch_specific 'env_set' $manifest $arch
|
||||
if ($env_set) {
|
||||
$env_set | Get-Member -Member NoteProperty | ForEach-Object {
|
||||
$name = $_.name
|
||||
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
|
||||
$name = $_.Name
|
||||
Set-EnvVar -Name $name -Value $null -Global:$global
|
||||
if (Test-Path env:\$name) { Remove-Item env:\$name }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-HookScript {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('pre_install', 'post_install',
|
||||
'pre_uninstall', 'post_uninstall')]
|
||||
[String] $HookType,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[PSCustomObject] $Manifest,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('32bit', '64bit', 'arm64')]
|
||||
[String] $Arch
|
||||
)
|
||||
|
||||
$script = arch_specific $HookType $Manifest $Arch
|
||||
if ($script) {
|
||||
Write-Output "Running $HookType script..."
|
||||
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
|
||||
}
|
||||
}
|
||||
|
||||
function show_notes($manifest, $dir, $original_dir, $persist_dir) {
|
||||
if ($manifest.notes) {
|
||||
Write-Output 'Notes'
|
||||
Write-Output '-----'
|
||||
Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }))
|
||||
Write-Output '-----'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,7 +420,7 @@ function show_suggestions($suggested) {
|
||||
}
|
||||
|
||||
if (!$fulfilled) {
|
||||
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'."
|
||||
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." -ForegroundColor DarkYellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,13 +110,13 @@ function json_path([String] $json, [String] $jsonpath, [Hashtable] $substitution
|
||||
# Return versions in reverse order
|
||||
$result = [System.Linq.Enumerable]::Reverse($result)
|
||||
}
|
||||
if ($single) {
|
||||
if ([System.Linq.Enumerable]::Count($result) -eq 1 -or $single) {
|
||||
# Extract First value
|
||||
$result = [System.Linq.Enumerable]::First($result)
|
||||
# Convert first value to string
|
||||
$result = $result.ToString()
|
||||
} else {
|
||||
$result = "$([String]::Join('\n', $result))"
|
||||
$result = [Newtonsoft.Json.JsonConvert]::SerializeObject($result)
|
||||
}
|
||||
return $result
|
||||
} catch [Exception] {
|
||||
|
||||
291
lib/manifest.ps1
291
lib/manifest.ps1
@@ -23,7 +23,7 @@ function url_manifest($url) {
|
||||
} catch {
|
||||
throw
|
||||
}
|
||||
if(!$str) { return $null }
|
||||
if (!$str) { return $null }
|
||||
try {
|
||||
$str | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
@@ -40,30 +40,74 @@ function Get-Manifest($app) {
|
||||
$app = appname_from_url $url
|
||||
$manifest = url_manifest $url
|
||||
} else {
|
||||
$app, $bucket, $version = parse_app $app
|
||||
if ($bucket) {
|
||||
$manifest = manifest $app $bucket
|
||||
# Check if the manifest is already installed
|
||||
if (installed $app) {
|
||||
$global = installed $app $true
|
||||
$ver = Select-CurrentVersion -AppName $app -Global:$global
|
||||
if (!$ver) {
|
||||
$app, $bucket, $ver = parse_app $app
|
||||
$ver = Select-CurrentVersion -AppName $app -Global:$global
|
||||
}
|
||||
$install_info_path = "$(versiondir $app $ver $global)\install.json"
|
||||
if (Test-Path $install_info_path) {
|
||||
$install_info = parse_json $install_info_path
|
||||
$bucket = $install_info.bucket
|
||||
if (!$bucket) {
|
||||
$url = $install_info.url
|
||||
if ($url -match '^(ht|f)tps?://|\\\\') {
|
||||
$manifest = url_manifest $url
|
||||
}
|
||||
if (!$manifest) {
|
||||
if (Test-Path $url) {
|
||||
$manifest = parse_json $url
|
||||
} else {
|
||||
# Fallback to installed manifest
|
||||
$manifest = installed_manifest $app $ver $global
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$manifest = manifest $app $bucket
|
||||
if (!$manifest) {
|
||||
$deprecated_dir = (Find-BucketDirectory -Name $bucket -Root) + '\deprecated'
|
||||
$manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($tekcub in Get-LocalBucket) {
|
||||
$manifest = manifest $app $tekcub
|
||||
if ($manifest) {
|
||||
$bucket = $tekcub
|
||||
break
|
||||
$app, $bucket, $version = parse_app $app
|
||||
if ($bucket) {
|
||||
$manifest = manifest $app $bucket
|
||||
} else {
|
||||
$matched_buckets = @()
|
||||
foreach ($tekcub in Get-LocalBucket) {
|
||||
$current_manifest = manifest $app $tekcub
|
||||
if (!$manifest -and $current_manifest) {
|
||||
$manifest = $current_manifest
|
||||
$bucket = $tekcub
|
||||
}
|
||||
if ($current_manifest) {
|
||||
$matched_buckets += $tekcub
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$manifest) {
|
||||
# couldn't find app in buckets: check if it's a local path
|
||||
if (Test-Path $app) {
|
||||
$url = Convert-Path $app
|
||||
$app = appname_from_url $url
|
||||
$manifest = parse_json $url
|
||||
} else {
|
||||
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
|
||||
$app = appname_from_url $app
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$manifest) {
|
||||
# couldn't find app in buckets: check if it's a local path
|
||||
if (Test-Path $app) {
|
||||
$url = Convert-Path $app
|
||||
$app = appname_from_url $url
|
||||
$manifest = url_manifest $url
|
||||
} else {
|
||||
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
|
||||
$app = appname_from_url $app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched_buckets.Length -gt 1) {
|
||||
warn "Multiple buckets contain manifest '$app', the current selection is '$bucket/$app'."
|
||||
}
|
||||
|
||||
return $app, $manifest, $bucket, $url
|
||||
}
|
||||
|
||||
@@ -128,33 +172,218 @@ function Get-SupportedArchitecture($manifest, $architecture) {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RelativePathCompat($from, $to) {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Cross-platform compatible relative path function
|
||||
.DESCRIPTION
|
||||
Falls back to custom implementation for Windows PowerShell compatibility
|
||||
#>
|
||||
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
||||
# PowerShell Core/7+ - use built-in method
|
||||
try {
|
||||
return [System.IO.Path]::GetRelativePath($from, $to)
|
||||
} catch {
|
||||
# Fallback if method fails
|
||||
}
|
||||
}
|
||||
|
||||
# Windows PowerShell compatible implementation
|
||||
$fromUri = New-Object System.Uri($from.TrimEnd('\') + '\')
|
||||
$toUri = New-Object System.Uri($to)
|
||||
|
||||
if ($fromUri.Scheme -ne $toUri.Scheme) {
|
||||
return $to # Cannot make relative path between different schemes
|
||||
}
|
||||
|
||||
$relativeUri = $fromUri.MakeRelativeUri($toUri)
|
||||
$relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())
|
||||
|
||||
return $relativePath -replace '/', '\'
|
||||
}
|
||||
|
||||
function Find-HistoricalManifestInCache($app, $bucket, $requestedVersion) {
|
||||
if (!(get_config USE_SQLITE_CACHE)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Return null if bucket is null or empty
|
||||
if (!$bucket) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Import database functions if not already loaded
|
||||
if (!(Get-Command 'Get-ScoopDBItem' -ErrorAction SilentlyContinue)) {
|
||||
. "$PSScriptRoot\database.ps1"
|
||||
}
|
||||
|
||||
$dbResult = Get-ScoopDBItem -Name $app -Bucket $bucket -Version $requestedVersion
|
||||
|
||||
# Strictly follow DB contract: must be DataTable with at least one row
|
||||
if (-not ($dbResult -is [System.Data.DataTable])) { return $null }
|
||||
if ($dbResult.Rows.Count -eq 0) { return $null }
|
||||
|
||||
$manifestText = $dbResult.Rows[0]['manifest']
|
||||
if ([string]::IsNullOrWhiteSpace($manifestText)) { return $null }
|
||||
|
||||
$manifestObj = $null
|
||||
try { $manifestObj = $manifestText | ConvertFrom-Json -ErrorAction Stop } catch {}
|
||||
$manifestVersion = if ($manifestObj -and $manifestObj.version) { $manifestObj.version } else { $requestedVersion }
|
||||
|
||||
return @{ ManifestText = $manifestText; version = $manifestVersion; source = "sqlite_exact_match" }
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-HistoricalManifestInGit($app, $bucket, $requestedVersion) {
|
||||
# Only proceed if git history is enabled
|
||||
if (!(get_config USE_GIT_HISTORY $true)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if (!$bucket) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$bucketDir = Find-BucketDirectory $bucket -Root
|
||||
if (!(Test-Path "$bucketDir\.git")) {
|
||||
warn "Bucket '$bucket' is not a git repository. Cannot search historical versions."
|
||||
return $null
|
||||
}
|
||||
|
||||
$manifestPath = "$app.json"
|
||||
$innerBucketDir = Find-BucketDirectory $bucket # Non-root path
|
||||
|
||||
if (-not (Test-Path $innerBucketDir -PathType Container)) {
|
||||
warn "Could not find inner bucket directory for '$bucket' at '$innerBucketDir'."
|
||||
return $null
|
||||
}
|
||||
$relativeManifestPath = Get-RelativePathCompat $bucketDir (Join-Path $innerBucketDir $manifestPath)
|
||||
$relativeManifestPath = $relativeManifestPath -replace '\\', '/'
|
||||
|
||||
try {
|
||||
# Prefer precise regex match on version line, fallback to -S literal
|
||||
$pattern = '"version"\s*:\s*"' + [regex]::Escape($requestedVersion) + '"'
|
||||
$commits = @()
|
||||
$outG = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-G',$pattern,'--',$relativeManifestPath)
|
||||
if ($outG) { $commits = @($outG | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
|
||||
|
||||
if ($commits.Count -eq 0) {
|
||||
$searchLiteral = '"version": "' + $requestedVersion + '"'
|
||||
$outS = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-S',$searchLiteral,'--',$relativeManifestPath)
|
||||
if ($outS) { $commits = @($outS | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
|
||||
}
|
||||
|
||||
if ($commits.Count -eq 0) { return $null }
|
||||
|
||||
$h = $commits[0]
|
||||
|
||||
# First try parent snapshot (latest state before change), then the change itself
|
||||
foreach ($spec in @("$h^","$h")) {
|
||||
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$spec`:$relativeManifestPath")
|
||||
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
|
||||
if ($content -is [Array]) { $content = $content -join "`n" }
|
||||
try {
|
||||
$obj = $content | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch { continue }
|
||||
if ($obj -and $obj.version -eq $requestedVersion) {
|
||||
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$spec" }
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback: iterate recent commits that touched the version string and validate
|
||||
$outAll = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','--format=%H','-G',$pattern,'--',$relativeManifestPath)
|
||||
$allCommits = @()
|
||||
if ($outAll) { $allCommits = @($outAll | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
|
||||
|
||||
foreach ($c in $allCommits) {
|
||||
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$c`:$relativeManifestPath")
|
||||
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
|
||||
if ($content -is [Array]) { $content = $content -join "`n" }
|
||||
try { $obj = $content | ConvertFrom-Json -ErrorAction Stop } catch { continue }
|
||||
if ($obj -and $obj.version -eq $requestedVersion) {
|
||||
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$c" }
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
function Find-HistoricalManifest($app, $bucket, $version) {
|
||||
# Orchestrates historical manifest lookup using available providers (DB → Git)
|
||||
$result = $null
|
||||
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
$result = Find-HistoricalManifestInCache $app $bucket $version
|
||||
if ($result) {
|
||||
if ($result.ManifestText) {
|
||||
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
|
||||
return @{ path = $path; version = $result.version; source = $result.source }
|
||||
}
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
if (get_config USE_GIT_HISTORY $true) {
|
||||
$result = Find-HistoricalManifestInGit $app $bucket $version
|
||||
if ($result) {
|
||||
if ($result.ManifestText) {
|
||||
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
|
||||
return @{ path = $path; version = $result.version; source = $result.source }
|
||||
}
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
|
||||
function generate_user_manifest($app, $bucket, $version) {
|
||||
# 'autoupdate.ps1' 'buckets.ps1' 'manifest.ps1'
|
||||
$app, $manifest, $bucket, $null = Get-Manifest "$bucket/$app"
|
||||
if ("$($manifest.version)" -eq "$version") {
|
||||
return manifest_path $app $bucket
|
||||
}
|
||||
warn "Given version ($version) does not match manifest ($($manifest.version))"
|
||||
warn "Attempting to generate manifest for '$app' ($version)"
|
||||
|
||||
# Try historical providers via orchestrator
|
||||
$historicalResult = Find-HistoricalManifest $app $bucket $version
|
||||
if ($historicalResult) { return $historicalResult.path }
|
||||
|
||||
# No historical manifest; try autoupdate if available
|
||||
if (!($manifest.autoupdate)) {
|
||||
abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'"
|
||||
abort "Could not find manifest for '$app@$version' and no autoupdate is available"
|
||||
}
|
||||
|
||||
ensure (usermanifestsdir) | out-null
|
||||
ensure (usermanifestsdir) | Out-Null
|
||||
$manifest_path = "$(usermanifestsdir)\$app.json"
|
||||
|
||||
try {
|
||||
Invoke-AutoUpdate $app "$(Convert-Path (usermanifestsdir))\$app.json" $manifest $version $(@{ })
|
||||
return Convert-Path (usermanifest $app)
|
||||
Invoke-AutoUpdate $app $manifest_path $manifest $version $(@{ })
|
||||
return $manifest_path
|
||||
} catch {
|
||||
write-host -f darkred "Could not install $app@$version"
|
||||
warn "Autoupdate failed for '$app@$version'"
|
||||
abort "Installation of '$app@$version' is not possible"
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function url($manifest, $arch) { arch_specific 'url' $manifest $arch }
|
||||
function installer($manifest, $arch) { arch_specific 'installer' $manifest $arch }
|
||||
function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $arch }
|
||||
function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch }
|
||||
function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch}
|
||||
function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch}
|
||||
function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch }
|
||||
function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch }
|
||||
|
||||
# Helper: write manifest text to user manifests cache directory and return path
|
||||
function Write-ManifestToUserCache {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)][string]$App,
|
||||
[Parameter(Mandatory=$true, Position=1)][string]$ManifestText
|
||||
)
|
||||
ensure (usermanifestsdir) | Out-Null
|
||||
$tempManifestPath = "$(usermanifestsdir)\$App.json"
|
||||
$ManifestText | Out-UTF8File -FilePath $tempManifestPath
|
||||
return $tempManifestPath
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ function env($name, $global, $val) {
|
||||
|
||||
function strip_path($orig_path, $dir) {
|
||||
Show-DeprecatedWarning $MyInvocation 'Split-PathLikeEnvVar'
|
||||
Split-PathLikeEnvVar -Name $dir -Path $orig_path
|
||||
Split-PathLikeEnvVar -Pattern @($dir) -Path $orig_path
|
||||
}
|
||||
|
||||
function add_first_in_path($dir, $global) {
|
||||
|
||||
@@ -1,117 +1,68 @@
|
||||
# Usage: scoop alias add|list|rm [<args>]
|
||||
# Usage: scoop alias <subcommand> [options] [<args>]
|
||||
# Summary: Manage scoop aliases
|
||||
# Help: Add, remove or list Scoop aliases
|
||||
# Help: Available subcommands: add, rm, list.
|
||||
#
|
||||
# Aliases are custom Scoop subcommands that can be created to make common tasks
|
||||
# easier.
|
||||
# Aliases are custom Scoop subcommands that can be created to make common tasks easier.
|
||||
#
|
||||
# To add an Alias:
|
||||
# scoop alias add <name> <command> <description>
|
||||
# To add an alias:
|
||||
#
|
||||
# e.g.:
|
||||
# scoop alias add rm 'scoop uninstall $args[0]' 'Uninstalls an app'
|
||||
# scoop alias add upgrade 'scoop update *' 'Updates all apps, just like brew or apt'
|
||||
# scoop alias add <name> <command> [<description>]
|
||||
#
|
||||
# e.g.,
|
||||
#
|
||||
# scoop alias add rm 'scoop uninstall $args[0]' 'Uninstall an app'
|
||||
# scoop alias add upgrade 'scoop update *' 'Update all apps, just like "brew" or "apt"'
|
||||
#
|
||||
# To remove an alias:
|
||||
#
|
||||
# scoop alias rm <name>
|
||||
#
|
||||
# To list all aliases:
|
||||
#
|
||||
# scoop alias list [-v|--verbose]
|
||||
#
|
||||
# Options:
|
||||
# -v, --verbose Show alias description and table headers (works only for 'list')
|
||||
# -v, --verbose Show alias description and table headers (works only for "list")
|
||||
|
||||
param(
|
||||
[String]$opt,
|
||||
[String]$name,
|
||||
[String]$command,
|
||||
[String]$description,
|
||||
[Switch]$verbose = $false
|
||||
)
|
||||
param($SubCommand)
|
||||
|
||||
. "$PSScriptRoot\..\lib\install.ps1" # shim related
|
||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||
|
||||
$script:config_alias = 'alias'
|
||||
|
||||
function init_alias_config {
|
||||
$aliases = get_config $script:config_alias
|
||||
if ($aliases) {
|
||||
$aliases
|
||||
$SubCommands = @('add', 'rm', 'list')
|
||||
if ($SubCommand -notin $SubCommands) {
|
||||
if (!$SubCommand) {
|
||||
error '<subcommand> missing'
|
||||
} else {
|
||||
New-Object -TypeName PSObject
|
||||
error "'$SubCommand' is not one of available subcommands: $($SubCommands -join ', ')"
|
||||
}
|
||||
my_usage
|
||||
exit 1
|
||||
}
|
||||
|
||||
function add_alias($name, $command) {
|
||||
if (!$command) {
|
||||
abort "Can't create an empty alias."
|
||||
$opt, $other, $err = getopt $Args 'v' 'verbose'
|
||||
if ($err) { "scoop alias: $err"; exit 1 }
|
||||
|
||||
$name, $command, $description = $other
|
||||
$verbose = $opt.v -or $opt.verbose
|
||||
|
||||
switch ($SubCommand) {
|
||||
'add' {
|
||||
if (!$name -or !$command) {
|
||||
error "<name> and <command> must be specified for subcommand 'add'"
|
||||
exit 1
|
||||
}
|
||||
add_alias $name $command $description
|
||||
}
|
||||
|
||||
# get current aliases from config
|
||||
$aliases = init_alias_config
|
||||
if ($aliases.$name) {
|
||||
abort "Alias '$name' already exists."
|
||||
'rm' {
|
||||
if (!$name) {
|
||||
error "<name> must be specified for subcommand 'rm'"
|
||||
exit 1
|
||||
}
|
||||
rm_alias $name
|
||||
}
|
||||
|
||||
$alias_file = "scoop-$name"
|
||||
|
||||
# generate script
|
||||
$shimdir = shimdir $false
|
||||
if (Test-Path "$shimdir\$alias_file.ps1") {
|
||||
abort "File '$alias_file.ps1' already exists in shims directory."
|
||||
'list' {
|
||||
list_aliases $verbose
|
||||
}
|
||||
$script =
|
||||
@(
|
||||
"# Summary: $description",
|
||||
"$command"
|
||||
) -join "`r`n"
|
||||
$script | Out-UTF8File "$shimdir\$alias_file.ps1"
|
||||
|
||||
# add alias to config
|
||||
$aliases | Add-Member -MemberType NoteProperty -Name $name -Value $alias_file
|
||||
|
||||
set_config $script:config_alias $aliases | Out-Null
|
||||
}
|
||||
|
||||
function rm_alias($name) {
|
||||
$aliases = init_alias_config
|
||||
if (!$name) {
|
||||
abort 'Alias to be removed has not been specified!'
|
||||
}
|
||||
|
||||
if ($aliases.$name) {
|
||||
info "Removing alias '$name'..."
|
||||
|
||||
rm_shim $aliases.$name (shimdir $false)
|
||||
|
||||
$aliases.PSObject.Properties.Remove($name)
|
||||
set_config $script:config_alias $aliases | Out-Null
|
||||
} else {
|
||||
abort "Alias '$name' doesn't exist."
|
||||
}
|
||||
}
|
||||
|
||||
function list_aliases {
|
||||
$aliases = @()
|
||||
|
||||
(init_alias_config).PSObject.Properties.GetEnumerator() | ForEach-Object {
|
||||
$content = Get-Content (command_path $_.Name)
|
||||
$command = ($content | Select-Object -Skip 1).Trim()
|
||||
$summary = (summary $content).Trim()
|
||||
|
||||
$aliases += New-Object psobject -Property @{Name = $_.name; Summary = $summary; Command = $command }
|
||||
}
|
||||
|
||||
if (!$aliases.count) {
|
||||
info "No alias found."
|
||||
}
|
||||
$aliases = $aliases.GetEnumerator() | Sort-Object Name
|
||||
if ($verbose) {
|
||||
return $aliases | Select-Object Name, Command, Summary
|
||||
} else {
|
||||
return $aliases | Select-Object Name, Command
|
||||
}
|
||||
}
|
||||
|
||||
switch ($opt) {
|
||||
'add' { add_alias $name $command }
|
||||
'rm' { rm_alias $name }
|
||||
'list' { list_aliases }
|
||||
default { my_usage; exit 1 }
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
# scoop bucket known
|
||||
param($cmd, $name, $repo)
|
||||
|
||||
if (get_config NO_JUNCTION) {
|
||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||
}
|
||||
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
}
|
||||
|
||||
$usage_add = 'usage: scoop bucket add <name> [<repo>]'
|
||||
$usage_rm = 'usage: scoop bucket rm <name>'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ param($cmd)
|
||||
|
||||
function cacheinfo($file) {
|
||||
$app, $version, $url = $file.Name -split '#'
|
||||
New-Object PSObject -Property @{ Name = $app; Version = $version; Length = $file.Length; URL = $url }
|
||||
New-Object PSObject -Property @{ Name = $app; Version = $version; Length = $file.Length }
|
||||
}
|
||||
|
||||
function cacheshow($app) {
|
||||
@@ -28,7 +28,7 @@ function cacheshow($app) {
|
||||
$files = @(Get-ChildItem $cachedir | Where-Object -Property Name -Value "^$app#" -Match)
|
||||
$totalLength = ($files | Measure-Object -Property Length -Sum).Sum
|
||||
|
||||
$files | ForEach-Object { cacheinfo $_ } | Select-Object Name, Version, Length, URL
|
||||
$files | ForEach-Object { cacheinfo $_ } | Select-Object Name, Version, Length
|
||||
|
||||
Write-Host "Total: $($files.Length) $(pluralize $files.Length 'file' 'files'), $(filesize $totalLength)" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -48,7 +48,7 @@ function cacheremove($app) {
|
||||
|
||||
$files | ForEach-Object {
|
||||
$curr = cacheinfo $_
|
||||
Write-Host "Removing $($curr.URL)..."
|
||||
Write-Host "Removing $($_.Name)..."
|
||||
Remove-Item $_.FullName
|
||||
if(Test-Path "$cachedir\$($curr.Name).txt") {
|
||||
Remove-Item "$cachedir\$($curr.Name).txt"
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
param($app)
|
||||
|
||||
. "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson'
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
|
||||
|
||||
if (!$app) { error '<app> missing'; my_usage; exit 1 }
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
# use_lessmsi: $true|$false
|
||||
# Prefer lessmsi utility over native msiexec.
|
||||
#
|
||||
# use_sqlite_cache: $true|$false
|
||||
# Use SQLite database for caching. This is useful for speeding up 'scoop search' and 'scoop shim' commands.
|
||||
#
|
||||
# use_git_history: $true|$false
|
||||
# Enable searching for specific versions in git history when installing apps with version specifiers.
|
||||
# When enabled, Scoop will first search the bucket's git history for the exact version before falling back to autoupdate.
|
||||
# (Default is $true)
|
||||
#
|
||||
# no_junction: $true|$false
|
||||
# The 'current' version alias will not be used. Shims and shortcuts will point to specific version instead.
|
||||
#
|
||||
@@ -163,7 +171,7 @@ if (!$name) {
|
||||
Write-Host "'$name' has been set to '$value'"
|
||||
} else {
|
||||
$value = get_config $name
|
||||
if($null -eq $value) {
|
||||
if ($null -eq $value) {
|
||||
Write-Host "'$name' is not set"
|
||||
} else {
|
||||
if ($value -is [System.DateTime]) {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' (indirectly)
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
|
||||
|
||||
$opt, $apps, $err = getopt $args 'a:' 'arch='
|
||||
$app = $apps[0]
|
||||
@@ -20,7 +22,14 @@ try {
|
||||
$deps = @()
|
||||
Get-Dependency $app $architecture | ForEach-Object {
|
||||
$dep = [ordered]@{}
|
||||
$dep.Source, $dep.Name = $_ -split '/'
|
||||
|
||||
$app, $null, $bucket, $url = Get-Manifest $_
|
||||
if (!$url) {
|
||||
$bucket, $app = $_ -split '/'
|
||||
}
|
||||
$dep.Source = if ($url) { $url } else { $bucket }
|
||||
$dep.Name = $app
|
||||
|
||||
$deps += [PSCustomObject]$dep
|
||||
}
|
||||
$deps
|
||||
|
||||
@@ -15,20 +15,25 @@
|
||||
#
|
||||
# Options:
|
||||
# -f, --force Force download (overwrite cache)
|
||||
# -h, --no-hash-check Skip hash verification (use with caution!)
|
||||
# -s, --skip-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\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
|
||||
. "$PSScriptRoot\..\lib\install.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1" # 'nightly_version'
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
}
|
||||
|
||||
$opt, $apps, $err = getopt $args 'fhua:' 'force', 'no-hash-check', 'no-update-scoop', 'arch='
|
||||
$opt, $apps, $err = getopt $args 'fsua:' 'force', 'skip-hash-check', 'no-update-scoop', 'arch='
|
||||
if ($err) { error "scoop download: $err"; exit 1 }
|
||||
|
||||
$check_hash = !($opt.h -or $opt.'no-hash-check')
|
||||
$check_hash = !($opt.s -or $opt.'skip-hash-check')
|
||||
$use_cache = !($opt.f -or $opt.force)
|
||||
$architecture = Get-DefaultArchitecture
|
||||
try {
|
||||
|
||||
@@ -29,14 +29,13 @@ if ($global -and !(is_admin)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$apps | ForEach-Object {
|
||||
$app = $_
|
||||
foreach ($app in $apps) {
|
||||
|
||||
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
|
||||
continue
|
||||
}
|
||||
if (!(installed $app $global)) {
|
||||
if ($global) {
|
||||
@@ -44,7 +43,7 @@ $apps | ForEach-Object {
|
||||
} else {
|
||||
error "'$app' is not installed."
|
||||
}
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
if (get_config NO_JUNCTION) {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# Summary: Opens the app homepage
|
||||
param($app)
|
||||
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
|
||||
|
||||
if ($app) {
|
||||
$null, $manifest, $bucket, $null = Get-Manifest $app
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
|
||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion'
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion', 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-RemoteFileSize'
|
||||
|
||||
$opt, $app, $err = getopt $args 'v' 'verbose'
|
||||
$original_app = $app
|
||||
if ($err) { error "scoop info: $err"; exit 1 }
|
||||
$verbose = $opt.v -or $opt.verbose
|
||||
|
||||
@@ -22,7 +24,7 @@ if (!$manifest) {
|
||||
$global = installed $app $true
|
||||
$status = app_status $app $global
|
||||
$install = install_info $app $status.version $global
|
||||
$status.installed = $bucket -and $install.bucket -eq $bucket
|
||||
$status.installed = ($bucket -and $install.bucket -eq $bucket) -or (installed $app)
|
||||
$version_output = $manifest.version
|
||||
$manifest_file = if ($bucket) {
|
||||
manifest_path $app $bucket
|
||||
@@ -30,12 +32,30 @@ $manifest_file = if ($bucket) {
|
||||
$url
|
||||
}
|
||||
|
||||
# Standalone and Source detection
|
||||
if ((Test-Path $original_app) -or ($original_app -match '^(ht|f)tps?://|\\\\')) {
|
||||
$standalone = $true
|
||||
if (Test-Path $original_app) {
|
||||
$original_app = (Get-AbsolutePath "$original_app")
|
||||
}
|
||||
if ($install.url) {
|
||||
if (Test-Path $install.url) {
|
||||
$install_url = (Get-AbsolutePath $install.url)
|
||||
} else {
|
||||
$install_url = $install.url
|
||||
}
|
||||
}
|
||||
if ($original_app -eq $install_url) {
|
||||
$same_source = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($verbose) {
|
||||
$dir = currentdir $app $global
|
||||
$original_dir = versiondir $app $manifest.version $global
|
||||
$persist_dir = persistdir $app $global
|
||||
} else {
|
||||
$dir, $original_dir, $persist_dir = "<root>", "<root>", "<root>"
|
||||
$dir, $original_dir, $persist_dir = '<root>', '<root>', '<root>'
|
||||
}
|
||||
|
||||
if ($status.installed) {
|
||||
@@ -43,21 +63,39 @@ if ($status.installed) {
|
||||
if ($install.url) {
|
||||
$manifest_file = $install.url
|
||||
}
|
||||
if ($status.version -eq $manifest.version) {
|
||||
if ($status.deprecated) {
|
||||
$manifest_file = $status.deprecated
|
||||
} elseif ($standalone -and !$same_source) {
|
||||
$version_output = $manifest.version
|
||||
} elseif ($status.version -eq $manifest.version) {
|
||||
$version_output = $status.version
|
||||
} else {
|
||||
$version_output = "$($status.version) (Update to $($manifest.version) available)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$item = [ordered]@{ Name = $app }
|
||||
if ($status.deprecated) {
|
||||
$item.Name += ' (DEPRECATED)'
|
||||
}
|
||||
if ($manifest.description) {
|
||||
$item.Description = $manifest.description
|
||||
}
|
||||
$item.Version = $version_output
|
||||
if ($bucket) {
|
||||
$item.Bucket = $bucket
|
||||
|
||||
$item.Source = if ($standalone) {
|
||||
$original_app
|
||||
} else {
|
||||
if ($install.bucket) {
|
||||
$install.bucket
|
||||
} elseif ($install.url) {
|
||||
$install.url
|
||||
} else {
|
||||
$bucket
|
||||
}
|
||||
}
|
||||
|
||||
if ($manifest.homepage) {
|
||||
$item.Website = $manifest.homepage.TrimEnd('/')
|
||||
}
|
||||
@@ -69,7 +107,7 @@ if ($manifest.license) {
|
||||
$manifest.license
|
||||
} elseif ($manifest.license -match '[|,]') {
|
||||
if ($verbose) {
|
||||
"$($manifest.license) ($(($manifest.license -Split "\||," | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
|
||||
"$($manifest.license) ($(($manifest.license -Split '\||,' | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
|
||||
} else {
|
||||
$manifest.license
|
||||
}
|
||||
@@ -84,7 +122,7 @@ if ($manifest.depends) {
|
||||
|
||||
if (Test-Path $manifest_file) {
|
||||
if (Get-Command git -ErrorAction Ignore) {
|
||||
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#'
|
||||
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aI#%an', $manifest_file) 2> $null) -Split '#'
|
||||
}
|
||||
if ($gitinfo) {
|
||||
$item.'Updated at' = $gitinfo[0] | Get-Date
|
||||
@@ -100,11 +138,13 @@ if ($verbose) { $item.Manifest = $manifest_file }
|
||||
|
||||
if ($status.installed) {
|
||||
# Show installed versions
|
||||
$installed_output = @()
|
||||
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
|
||||
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { " *global*" })" }
|
||||
if (!$standalone -or $same_source) {
|
||||
$installed_output = @()
|
||||
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
|
||||
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { ' *global*' })" }
|
||||
}
|
||||
$item.Installed = $installed_output -join "`n"
|
||||
}
|
||||
$item.Installed = $installed_output -join "`n"
|
||||
|
||||
if ($verbose) {
|
||||
# Show size of installation
|
||||
@@ -161,12 +201,12 @@ if ($status.installed) {
|
||||
foreach ($url in @(url $manifest (Get-DefaultArchitecture))) {
|
||||
try {
|
||||
if (Test-Path (cache_path $app $manifest.version $url)) {
|
||||
$cached = " (latest version is cached)"
|
||||
$cached = ' (latest version is cached)'
|
||||
} else {
|
||||
$cached = $null
|
||||
}
|
||||
|
||||
[int]$urlLength = (Invoke-WebRequest $url -Method Head).Headers.'Content-Length'[0]
|
||||
$urlLength = Get-RemoteFileSize $url
|
||||
$totalPackage += $urlLength
|
||||
} catch [System.Management.Automation.RuntimeException] {
|
||||
$totalPackage = 0
|
||||
@@ -196,7 +236,7 @@ if ($binaries) {
|
||||
$binary_output += $_
|
||||
}
|
||||
}
|
||||
$item.Binaries = $binary_output -join " | "
|
||||
$item.Binaries = $binary_output -join ' | '
|
||||
}
|
||||
$shortcuts = @(arch_specific 'shortcuts' $manifest $install.architecture)
|
||||
if ($shortcuts) {
|
||||
@@ -204,13 +244,13 @@ if ($shortcuts) {
|
||||
$shortcuts | ForEach-Object {
|
||||
$shortcut_output += $_[1]
|
||||
}
|
||||
$item.Shortcuts = $shortcut_output -join " | "
|
||||
$item.Shortcuts = $shortcut_output -join ' | '
|
||||
}
|
||||
$env_set = arch_specific 'env_set' $manifest $install.architecture
|
||||
if ($env_set) {
|
||||
$env_vars = @()
|
||||
$env_set | Get-Member -member noteproperty | ForEach-Object {
|
||||
$env_vars += "$($_.name) = $(format $env_set.$($_.name) @{ "dir" = $dir })"
|
||||
$env_vars += "$($_.name) = $(substitute $env_set.$($_.name) @{ '$dir' = $dir })"
|
||||
}
|
||||
$item.Environment = $env_vars -join "`n"
|
||||
}
|
||||
|
||||
@@ -4,21 +4,27 @@
|
||||
# scoop install git
|
||||
#
|
||||
# To install a different version of the app
|
||||
# (note that this will auto-generate the manifest using current version):
|
||||
# (will search sqlite cache if enabled, then git history (if use_git_history is $true), then auto-generate manifest as fallback):
|
||||
# scoop install gh@2.7.0
|
||||
#
|
||||
# To install an app from a manifest at a URL:
|
||||
# scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/runat.json
|
||||
#
|
||||
# To install a different version of the app from a URL:
|
||||
# scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/neovim.json@0.9.0
|
||||
#
|
||||
# To install an app from a manifest on your computer
|
||||
# scoop install \path\to\app.json
|
||||
#
|
||||
# To install an app from a manifest on your computer
|
||||
# scoop install \path\to\app.json@version
|
||||
#
|
||||
# Options:
|
||||
# -g, --global Install the app globally
|
||||
# -i, --independent Don't install dependencies automatically
|
||||
# -k, --no-cache Don't use the download cache
|
||||
# -s, --skip-hash-check Skip hash validation (use with caution!)
|
||||
# -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"
|
||||
@@ -27,17 +33,21 @@
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
|
||||
. "$PSScriptRoot\..\lib\system.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
. "$PSScriptRoot\..\lib\decompress.ps1"
|
||||
. "$PSScriptRoot\..\lib\shortcuts.ps1"
|
||||
. "$PSScriptRoot\..\lib\psmodules.ps1"
|
||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||
. "$PSScriptRoot\..\lib\depends.ps1"
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
}
|
||||
|
||||
$opt, $apps, $err = getopt $args 'gikusa:' 'global', 'independent', 'no-cache', 'no-update-scoop', 'skip', 'arch='
|
||||
$opt, $apps, $err = getopt $args 'giksua:' 'global', 'independent', 'no-cache', 'skip-hash-check', 'no-update-scoop', 'arch='
|
||||
if ($err) { "scoop install: $err"; exit 1 }
|
||||
|
||||
$global = $opt.g -or $opt.global
|
||||
$check_hash = !($opt.s -or $opt.skip)
|
||||
$check_hash = !($opt.s -or $opt.'skip-hash-check')
|
||||
$independent = $opt.i -or $opt.independent
|
||||
$use_cache = !($opt.k -or $opt.'no-cache')
|
||||
$architecture = Get-DefaultArchitecture
|
||||
@@ -82,7 +92,7 @@ $specific_versions = $apps | Where-Object {
|
||||
}
|
||||
|
||||
# compare object does not like nulls
|
||||
if ($specific_versions.length -gt 0) {
|
||||
if ($specific_versions.Count -gt 0) {
|
||||
$difference = Compare-Object -ReferenceObject $apps -DifferenceObject $specific_versions -PassThru
|
||||
} else {
|
||||
$difference = $apps
|
||||
@@ -91,13 +101,13 @@ if ($specific_versions.length -gt 0) {
|
||||
$specific_versions_paths = $specific_versions | ForEach-Object {
|
||||
$app, $bucket, $version = parse_app $_
|
||||
if (installed_manifest $app $version) {
|
||||
warn "'$app' ($version) is already installed.`nUse 'scoop update $app$(if ($global) { " --global" })' to install a new version."
|
||||
warn "'$app' ($version) is already installed.`nUse 'scoop update $app$(if ($global) { ' --global' })' to install a new version."
|
||||
continue
|
||||
}
|
||||
|
||||
generate_user_manifest $app $bucket $version
|
||||
}
|
||||
$apps = @(($specific_versions_paths + $difference) | Where-Object { $_ } | Sort-Object -Unique)
|
||||
$apps = @((@($specific_versions_paths) + $difference) | Where-Object { $_ } | Select-Object -Unique)
|
||||
|
||||
# remember which were explictly requested so that we can
|
||||
# differentiate after dependencies are added
|
||||
|
||||
@@ -5,8 +5,9 @@ param($query)
|
||||
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'parse_json' 'Select-CurrentVersion' (indirectly)
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
|
||||
|
||||
$def_arch = Get-DefaultArchitecture
|
||||
$defaultArchitecture = Get-DefaultArchitecture
|
||||
if (-not (Get-FormatData ScoopApps)) {
|
||||
Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml"
|
||||
}
|
||||
@@ -47,10 +48,11 @@ $apps | Where-Object { !$query -or ($_.name -match $query) } | ForEach-Object {
|
||||
$item.Updated = $updated
|
||||
|
||||
$info = @()
|
||||
if ((app_status $app $global).deprecated) { $info += 'Deprecated package'}
|
||||
if ($global) { $info += 'Global install' }
|
||||
if (failed $app $global) { $info += 'Install failed' }
|
||||
if ($install_info.hold) { $info += 'Held package' }
|
||||
if ($install_info.architecture -and $def_arch -ne $install_info.architecture) {
|
||||
if ($install_info.architecture -and $defaultArchitecture -ne $install_info.architecture) {
|
||||
$info += $install_info.architecture
|
||||
}
|
||||
$item.Info = $info -join ', '
|
||||
|
||||
@@ -84,7 +84,7 @@ $apps | ForEach-Object {
|
||||
env_rm_path $manifest $dir $global $architecture
|
||||
env_rm $manifest $global $architecture
|
||||
env_add_path $manifest $dir $global $architecture
|
||||
env_set $manifest $dir $global $architecture
|
||||
env_set $manifest $global $architecture
|
||||
# unlink all potential old link before re-persisting
|
||||
unlink_persist_data $manifest $original_dir
|
||||
persist_data $manifest $original_dir $persist_dir
|
||||
|
||||
@@ -3,26 +3,17 @@
|
||||
# Help: Searches for apps that are available to install.
|
||||
#
|
||||
# If used with [query], shows app names that match the query.
|
||||
# - With 'use_sqlite_cache' enabled, [query] is partially matched against app names, binaries, and shortcuts.
|
||||
# - Without 'use_sqlite_cache', [query] can be a regular expression to match against app names and binaries.
|
||||
# Without [query], shows all the available apps.
|
||||
param($query)
|
||||
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest'
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion'
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
|
||||
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
try {
|
||||
$query = New-Object Regex $query, 'IgnoreCase'
|
||||
} catch {
|
||||
abort "Invalid regular expression: $($_.Exception.InnerException.Message)"
|
||||
}
|
||||
|
||||
$githubtoken = Get-GitHubToken
|
||||
$authheader = @{}
|
||||
if ($githubtoken) {
|
||||
$authheader = @{'Authorization' = "token $githubtoken"}
|
||||
}
|
||||
|
||||
function bin_match($manifest, $query) {
|
||||
if (!$manifest.bin) { return $false }
|
||||
$bins = foreach ($bin in $manifest.bin) {
|
||||
@@ -39,16 +30,16 @@ function bin_match($manifest, $query) {
|
||||
|
||||
function bin_match_json($json, $query) {
|
||||
[System.Text.Json.JsonElement]$bin = [System.Text.Json.JsonElement]::new()
|
||||
if (!$json.RootElement.TryGetProperty("bin", [ref] $bin)) { return $false }
|
||||
if (!$json.RootElement.TryGetProperty('bin', [ref] $bin)) { return $false }
|
||||
$bins = @()
|
||||
if($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) {
|
||||
if ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) {
|
||||
$bins += [System.IO.Path]::GetFileName($bin)
|
||||
} elseif ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
|
||||
foreach($subbin in $bin.EnumerateArray()) {
|
||||
if($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) {
|
||||
foreach ($subbin in $bin.EnumerateArray()) {
|
||||
if ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) {
|
||||
$bins += [System.IO.Path]::GetFileName($subbin)
|
||||
} elseif ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
|
||||
if([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) {
|
||||
if ([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) {
|
||||
$bins += [System.IO.Path]::GetFileName($subbin[0])
|
||||
} elseif ($subbin.GetArrayLength() -ge 2 -and $subbin[1] -match $query) {
|
||||
$bins += $subbin[1]
|
||||
@@ -65,25 +56,33 @@ function search_bucket($bucket, $query) {
|
||||
$apps = Get-ChildItem (Find-BucketDirectory $bucket) -Filter '*.json' -Recurse
|
||||
|
||||
$apps | ForEach-Object {
|
||||
$json = [System.Text.Json.JsonDocument]::Parse([System.IO.File]::ReadAllText($_.FullName))
|
||||
$filepath = $_.FullName
|
||||
|
||||
$json = try {
|
||||
[System.Text.Json.JsonDocument]::Parse([System.IO.File]::ReadAllText($filepath))
|
||||
} catch {
|
||||
debug "Failed to parse manifest file: $filepath (error: $_)"
|
||||
return
|
||||
}
|
||||
|
||||
$name = $_.BaseName
|
||||
|
||||
if ($name -match $query) {
|
||||
$list.Add([PSCustomObject]@{
|
||||
Name = $name
|
||||
Version = $json.RootElement.GetProperty("version")
|
||||
Source = $bucket
|
||||
Binaries = ""
|
||||
})
|
||||
Name = $name
|
||||
Version = $json.RootElement.GetProperty('version')
|
||||
Source = $bucket
|
||||
Binaries = ''
|
||||
})
|
||||
} else {
|
||||
$bin = bin_match_json $json $query
|
||||
if ($bin) {
|
||||
$list.Add([PSCustomObject]@{
|
||||
Name = $name
|
||||
Version = $json.RootElement.GetProperty("version")
|
||||
Source = $bucket
|
||||
Binaries = $bin -join ' | '
|
||||
})
|
||||
Name = $name
|
||||
Version = $json.RootElement.GetProperty('version')
|
||||
Source = $bucket
|
||||
Binaries = $bin -join ' | '
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,42 +98,25 @@ function search_bucket_legacy($bucket, $query) {
|
||||
|
||||
if ($name -match $query) {
|
||||
$list.Add([PSCustomObject]@{
|
||||
Name = $name
|
||||
Version = $manifest.Version
|
||||
Source = $bucket
|
||||
Binaries = ""
|
||||
})
|
||||
Name = $name
|
||||
Version = $manifest.Version
|
||||
Source = $bucket
|
||||
Binaries = ''
|
||||
})
|
||||
} else {
|
||||
$bin = bin_match $manifest $query
|
||||
if ($bin) {
|
||||
$list.Add([PSCustomObject]@{
|
||||
Name = $name
|
||||
Version = $manifest.Version
|
||||
Source = $bucket
|
||||
Binaries = $bin -join ' | '
|
||||
})
|
||||
Name = $name
|
||||
Version = $manifest.Version
|
||||
Source = $bucket
|
||||
Binaries = $bin -join ' | '
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function download_json($url) {
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
|
||||
$ProgressPreference = 'Continue'
|
||||
$result
|
||||
}
|
||||
|
||||
function github_ratelimit_reached {
|
||||
$api_link = 'https://api.github.com/rate_limit'
|
||||
$ret = (download_json $api_link).rate.remaining -eq 0
|
||||
if ($ret) {
|
||||
Write-Host "GitHub API rate limit reached.
|
||||
Please try again later or configure your API token using 'scoop config gh_token <your token>'."
|
||||
}
|
||||
$ret
|
||||
}
|
||||
|
||||
function search_remote($bucket, $query) {
|
||||
$uri = [System.Uri](known_bucket_repo $bucket)
|
||||
if ($uri.AbsolutePath -match '/([a-zA-Z0-9]*)/([a-zA-Z0-9-]*)(?:.git|/)?') {
|
||||
@@ -154,7 +136,7 @@ function search_remotes($query) {
|
||||
$names = $buckets | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
|
||||
|
||||
$results = $names | Where-Object { !(Test-Path $(Find-BucketDirectory $_)) } | ForEach-Object {
|
||||
@{ "bucket" = $_; "results" = (search_remote $_ $query) }
|
||||
@{ 'bucket' = $_; 'results' = (search_remote $_ $query) }
|
||||
} | Where-Object { $_.results }
|
||||
|
||||
if ($results.count -gt 0) {
|
||||
@@ -175,25 +157,45 @@ function search_remotes($query) {
|
||||
$remote_list
|
||||
}
|
||||
|
||||
$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq "System.Text.Json" }
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
Select-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
|
||||
Select-Object -Property name, version, bucket, binary |
|
||||
ForEach-Object {
|
||||
$list.Add([PSCustomObject]@{
|
||||
Name = $_.name
|
||||
Version = $_.version
|
||||
Source = $_.bucket
|
||||
Binaries = $_.binary
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$query = New-Object Regex $query, 'IgnoreCase'
|
||||
} catch {
|
||||
abort "Invalid regular expression: $($_.Exception.InnerException.Message)"
|
||||
}
|
||||
|
||||
Get-LocalBucket | ForEach-Object {
|
||||
if ($jsonTextAvailable) {
|
||||
search_bucket $_ $query
|
||||
} else {
|
||||
search_bucket_legacy $_ $query
|
||||
$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq 'System.Text.Json' }
|
||||
|
||||
Get-LocalBucket | ForEach-Object {
|
||||
if ($jsonTextAvailable) {
|
||||
search_bucket $_ $query
|
||||
} else {
|
||||
search_bucket_legacy $_ $query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($list.Count -gt 0) {
|
||||
Write-Host "Results from local buckets..."
|
||||
Write-Host 'Results from local buckets...'
|
||||
$list
|
||||
}
|
||||
|
||||
if ($list.Count -eq 0 -and !(github_ratelimit_reached)) {
|
||||
$remote_results = search_remotes $query
|
||||
if (!$remote_results) {
|
||||
warn "No matches found."
|
||||
warn 'No matches found.'
|
||||
exit 1
|
||||
}
|
||||
$remote_results
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest' 'parse_json' "install_info"
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
|
||||
|
||||
# check if scoop needs updating
|
||||
$currentdir = versiondir 'scoop' 'current'
|
||||
@@ -58,7 +59,7 @@ $true, $false | ForEach-Object { # local and global apps
|
||||
Get-ChildItem $dir | Where-Object name -NE 'scoop' | ForEach-Object {
|
||||
$app = $_.name
|
||||
$status = app_status $app $global
|
||||
if (!$status.outdated -and !$status.failed -and !$status.removed -and !$status.missing_deps) { return }
|
||||
if (!$status.outdated -and !$status.failed -and !$status.deprecated -and !$status.removed -and !$status.missing_deps) { return }
|
||||
|
||||
$item = [ordered]@{}
|
||||
$item.Name = $app
|
||||
@@ -66,9 +67,10 @@ $true, $false | ForEach-Object { # local and global apps
|
||||
$item.'Latest Version' = if ($status.outdated) { $status.latest_version } else { "" }
|
||||
$item.'Missing Dependencies' = $status.missing_deps -Split ' ' -Join ' | '
|
||||
$info = @()
|
||||
if ($status.failed) { $info += 'Install failed' }
|
||||
if ($status.hold) { $info += 'Held package' }
|
||||
if ($status.removed) { $info += 'Manifest removed' }
|
||||
if ($status.failed) { $info += 'Install failed' }
|
||||
if ($status.hold) { $info += 'Held package' }
|
||||
if ($status.deprecated) { $info += 'Deprecated' }
|
||||
if ($status.removed) { $info += 'Manifest removed' }
|
||||
$item.Info = $info -join ', '
|
||||
$list += [PSCustomObject]$item
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ if (!$apps) { exit 0 }
|
||||
$manifest = installed_manifest $app $version $global
|
||||
$install = install_info $app $version $global
|
||||
$architecture = $install.architecture
|
||||
$bucket = $install.bucket
|
||||
|
||||
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture
|
||||
|
||||
@@ -74,7 +75,7 @@ if (!$apps) { exit 0 }
|
||||
continue
|
||||
}
|
||||
|
||||
run_uninstaller $manifest $architecture $dir
|
||||
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global $global -Uninstall
|
||||
rm_shims $app $manifest $global $architecture
|
||||
rm_startmenu_shortcuts $manifest $global $architecture
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
# You can use '*' in place of <app> to update all apps.
|
||||
#
|
||||
# Options:
|
||||
# -f, --force Force update even when there isn't a newer version
|
||||
# -g, --global Update a globally installed app
|
||||
# -i, --independent Don't install dependencies automatically
|
||||
# -k, --no-cache Don't use the download cache
|
||||
# -s, --skip Skip hash validation (use with caution!)
|
||||
# -q, --quiet Hide extraneous messages
|
||||
# -a, --all Update all apps (alternative to '*')
|
||||
# -f, --force Force update even when there isn't a newer version
|
||||
# -g, --global Update a globally installed app
|
||||
# -i, --independent Don't install dependencies automatically
|
||||
# -k, --no-cache Don't use the download cache
|
||||
# -s, --skip-hash-check Skip hash validation (use with caution!)
|
||||
# -q, --quiet Hide extraneous messages
|
||||
# -a, --all Update all apps (alternative to '*')
|
||||
|
||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||
. "$PSScriptRoot\..\lib\json.ps1" # 'save_install_info' in 'manifest.ps1' (indirectly)
|
||||
@@ -24,12 +24,16 @@
|
||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||
. "$PSScriptRoot\..\lib\depends.ps1"
|
||||
. "$PSScriptRoot\..\lib\install.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
}
|
||||
|
||||
$opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip', 'quiet', 'all'
|
||||
$opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip-hash-check', 'quiet', 'all'
|
||||
if ($err) { "scoop update: $err"; exit 1 }
|
||||
$global = $opt.g -or $opt.global
|
||||
$force = $opt.f -or $opt.force
|
||||
$check_hash = !($opt.s -or $opt.skip)
|
||||
$check_hash = !($opt.s -or $opt.'skip-hash-check')
|
||||
$use_cache = !($opt.k -or $opt.'no-cache')
|
||||
$quiet = $opt.q -or $opt.quiet
|
||||
$independent = $opt.i -or $opt.independent
|
||||
@@ -38,21 +42,21 @@ $all = $opt.a -or $opt.all
|
||||
# load config
|
||||
$configRepo = get_config SCOOP_REPO
|
||||
if (!$configRepo) {
|
||||
$configRepo = "https://github.com/ScoopInstaller/Scoop"
|
||||
$configRepo = 'https://github.com/ScoopInstaller/Scoop'
|
||||
set_config SCOOP_REPO $configRepo | Out-Null
|
||||
}
|
||||
|
||||
# Find current update channel from config
|
||||
$configBranch = get_config SCOOP_BRANCH
|
||||
if (!$configBranch) {
|
||||
$configBranch = "master"
|
||||
$configBranch = 'master'
|
||||
set_config SCOOP_BRANCH $configBranch | Out-Null
|
||||
}
|
||||
|
||||
if(($PSVersionTable.PSVersion.Major) -lt 5) {
|
||||
if (($PSVersionTable.PSVersion.Major) -lt 5) {
|
||||
# check powershell version
|
||||
Write-Output "PowerShell 5 or later is required to run Scoop."
|
||||
Write-Output "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows"
|
||||
Write-Output 'PowerShell 5 or later is required to run Scoop.'
|
||||
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
|
||||
@@ -63,14 +67,14 @@ function Sync-Scoop {
|
||||
[Switch]$Log
|
||||
)
|
||||
# Test if Scoop Core is hold
|
||||
if(Test-ScoopCoreOnHold) {
|
||||
if (Test-ScoopCoreOnHold) {
|
||||
return
|
||||
}
|
||||
|
||||
# check for git
|
||||
if (!(Test-GitAvailable)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." }
|
||||
|
||||
Write-Host "Updating Scoop..."
|
||||
Write-Host 'Updating Scoop...'
|
||||
$currentdir = versiondir 'scoop' 'current'
|
||||
if (!(Test-Path "$currentdir\.git")) {
|
||||
$newdir = "$currentdir\..\new"
|
||||
@@ -108,10 +112,10 @@ function Sync-Scoop {
|
||||
# Stash uncommitted changes
|
||||
if (Invoke-Git -Path $currentdir -ArgumentList @('diff', 'HEAD', '--name-only')) {
|
||||
if (get_config AUTOSTASH_ON_CONFLICT) {
|
||||
warn "Uncommitted changes detected. Stashing..."
|
||||
warn 'Uncommitted changes detected. Stashing...'
|
||||
Invoke-Git -Path $currentdir -ArgumentList @('stash', 'push', '-m', "WIP at $([System.DateTime]::Now.ToString('o'))", '-u', '-q')
|
||||
} else {
|
||||
warn "Uncommitted changes detected. Update aborted."
|
||||
warn 'Uncommitted changes detected. Update aborted.'
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -132,7 +136,7 @@ function Sync-Scoop {
|
||||
# reset branch HEAD
|
||||
Invoke-Git -Path $currentdir -ArgumentList @('reset', '--hard', "origin/$configBranch", '-q')
|
||||
} else {
|
||||
Invoke-Git -Path $currentdir -ArgumentList @('pull', '-q')
|
||||
Invoke-Git -Path $currentdir -ArgumentList @('pull', '--tags', '--force', '-q')
|
||||
}
|
||||
|
||||
$res = $lastexitcode
|
||||
@@ -152,7 +156,7 @@ function Sync-Bucket {
|
||||
Param (
|
||||
[Switch]$Log
|
||||
)
|
||||
Write-Host "Updating Buckets..."
|
||||
Write-Host 'Updating Buckets...'
|
||||
|
||||
if (!(Test-Path (Join-Path (Find-BucketDirectory 'main' -Root) '.git'))) {
|
||||
info "Converting 'main' bucket to git repo..."
|
||||
@@ -170,40 +174,88 @@ function Sync-Bucket {
|
||||
$buckets = Get-LocalBucket | ForEach-Object {
|
||||
$path = Find-BucketDirectory $_ -Root
|
||||
return @{
|
||||
name = $_
|
||||
name = $_
|
||||
valid = Test-Path (Join-Path $path '.git')
|
||||
path = $path
|
||||
path = $path
|
||||
}
|
||||
}
|
||||
|
||||
$buckets | Where-Object { !$_.valid } | ForEach-Object { Write-Host "'$($_.name)' is not a git repository. Skipped." }
|
||||
|
||||
$updatedFiles = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
||||
$removedFiles = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
|
||||
if ($PSVersionTable.PSVersion.Major -ge 7) {
|
||||
# Parallel parameter is available since PowerShell 7
|
||||
$buckets | Where-Object { $_.valid } | ForEach-Object -ThrottleLimit 5 -Parallel {
|
||||
. "$using:PSScriptRoot\..\lib\core.ps1"
|
||||
. "$using:PSScriptRoot\..\lib\buckets.ps1"
|
||||
|
||||
$bucketLoc = $_.path
|
||||
$name = $_.name
|
||||
$bucketLoc = $_.path
|
||||
$innerBucketLoc = Find-BucketDirectory $name
|
||||
|
||||
$previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD')
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q')
|
||||
if ($using:Log) {
|
||||
Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit
|
||||
}
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-status', $previousCommit) | ForEach-Object {
|
||||
$status, $file = $_ -split '\s+', 2
|
||||
$filePath = Join-Path $bucketLoc $file
|
||||
if ($filePath -match "^$([regex]::Escape($innerBucketLoc)).*\.json$") {
|
||||
switch ($status) {
|
||||
{ $_ -in 'A', 'M', 'R' } {
|
||||
[void]($using:updatedFiles).Add($filePath)
|
||||
}
|
||||
'D' {
|
||||
[void]($using:removedFiles).Add([pscustomobject]@{
|
||||
Name = ([System.IO.FileInfo]$file).BaseName
|
||||
Bucket = $name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$buckets | Where-Object { $_.valid } | ForEach-Object {
|
||||
$bucketLoc = $_.path
|
||||
$name = $_.name
|
||||
$bucketLoc = $_.path
|
||||
$innerBucketLoc = Find-BucketDirectory $name
|
||||
|
||||
$previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD')
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q')
|
||||
if ($Log) {
|
||||
Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit
|
||||
}
|
||||
if (get_config USE_SQLITE_CACHE) {
|
||||
Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-status', $previousCommit) | ForEach-Object {
|
||||
$status, $file = $_ -split '\s+', 2
|
||||
$filePath = Join-Path $bucketLoc $file
|
||||
if ($filePath -match "^$([regex]::Escape($innerBucketLoc)).*\.json$") {
|
||||
switch ($status) {
|
||||
{ $_ -in 'A', 'M', 'R' } {
|
||||
[void]($updatedFiles).Add($filePath)
|
||||
}
|
||||
'D' {
|
||||
[void]($removedFiles).Add([pscustomobject]@{
|
||||
Name = ([System.IO.FileInfo]$file).BaseName
|
||||
Bucket = $name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((get_config USE_SQLITE_CACHE) -and ($updatedFiles.Count -gt 0 -or $removedFiles.Count -gt 0)) {
|
||||
info 'Updating cache...'
|
||||
Set-ScoopDB -Path $updatedFiles
|
||||
$removedFiles | Remove-ScoopDBItem
|
||||
}
|
||||
}
|
||||
|
||||
function update($app, $global, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) {
|
||||
@@ -251,7 +303,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
|
||||
# region Workaround
|
||||
# Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored
|
||||
# Remove and replace whole region after proper fix
|
||||
Write-Host "Downloading new version"
|
||||
Write-Host 'Downloading new version'
|
||||
if (Test-Aria2Enabled) {
|
||||
Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash
|
||||
} else {
|
||||
@@ -269,12 +321,12 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
|
||||
error $err
|
||||
if (Test-Path $source) {
|
||||
# rm cached file
|
||||
Remove-Item -force $source
|
||||
Remove-Item -Force $source
|
||||
}
|
||||
if ($url.Contains('sourceforge.net')) {
|
||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||
}
|
||||
abort $(new_issue_msg $app $bucket "hash check failed")
|
||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,7 +341,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
|
||||
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture
|
||||
|
||||
Write-Host "Uninstalling '$app' ($old_version)"
|
||||
run_uninstaller $old_manifest $architecture $dir
|
||||
Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Uninstall
|
||||
rm_shims $app $old_manifest $global $architecture
|
||||
|
||||
# If a junction was used during install, that will have been used
|
||||
@@ -404,11 +456,11 @@ if (-not ($apps -or $all)) {
|
||||
} elseif ($outdated.Length -eq 0) {
|
||||
Write-Host -f Green "Latest versions for all apps are installed! For more information try 'scoop status'"
|
||||
} else {
|
||||
Write-Host -f DarkCyan "Updating one outdated app:"
|
||||
Write-Host -f DarkCyan 'Updating one outdated app:'
|
||||
}
|
||||
}
|
||||
|
||||
$suggested = @{};
|
||||
$suggested = @{}
|
||||
# $outdated is a list of ($app, $global) tuples
|
||||
$outdated | ForEach-Object { update @_ $quiet $independent $suggested $use_cache $check_hash }
|
||||
}
|
||||
|
||||
@@ -29,15 +29,17 @@
|
||||
# -p, --passthru Return reports as objects
|
||||
|
||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||
. "$PSScriptRoot\..\lib\json.ps1" # 'json_path'
|
||||
. "$PSScriptRoot\..\lib\install.ps1" # 'hash_for_url'
|
||||
. "$PSScriptRoot\..\lib\download.ps1" # 'hash_for_url'
|
||||
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
|
||||
|
||||
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
|
||||
if ($err) { "scoop virustotal: $err"; exit 1 }
|
||||
if (!$apps -and -$all) { my_usage; exit 1 }
|
||||
$architecture = Format-ArchitectureString
|
||||
$all = $apps -eq '*' -or $opt.a -or $opt.all
|
||||
if (!$apps -and !$all) { my_usage; exit 1 }
|
||||
$architecture = Get-DefaultArchitecture
|
||||
|
||||
if (is_scoop_outdated) {
|
||||
if ($opt.u -or $opt.'no-update-scoop') {
|
||||
@@ -47,11 +49,8 @@ if (is_scoop_outdated) {
|
||||
}
|
||||
}
|
||||
|
||||
$apps_param = $apps
|
||||
|
||||
if ($apps_param -eq '*' -or $opt.a -or $opt.all) {
|
||||
$apps = installed_apps $false
|
||||
$apps += installed_apps $true
|
||||
if ($all) {
|
||||
$apps = (installed_apps $false) + (installed_apps $true)
|
||||
}
|
||||
|
||||
if (!$opt.n -and !$opt.'no-depends') {
|
||||
@@ -88,11 +87,6 @@ Function ConvertTo-VirusTotalUrlId ($url) {
|
||||
$url_id
|
||||
}
|
||||
|
||||
Function Get-RemoteFileSize ($url) {
|
||||
$response = Invoke-WebRequest -Uri $url -Method HEAD -UseBasicParsing
|
||||
$response.Headers.'Content-Length' | ForEach-Object { [System.Convert]::ToInt32($_) }
|
||||
}
|
||||
|
||||
Function Get-VirusTotalResultByHash ($hash, $url, $app) {
|
||||
$hash = $hash.ToLower()
|
||||
$api_url = "https://www.virustotal.com/api/v3/files/$hash"
|
||||
|
||||
@@ -1 +1 @@
|
||||
410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe
|
||||
140e3801d8adeda639a21b14e62b93a4c7d26b7a758421f43c82be59753be49b *shim.exe
|
||||
|
||||
@@ -1 +1 @@
|
||||
9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe
|
||||
59d9da9f9714003b915bcafbe1b41f53b121dde206ecc23984f62273e957766eece8d64ffc53011c328d3a2ad627aa0f4f7c39bbec8e7b64d0d2ee7b7e771423 *shim.exe
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
v3.1.1
|
||||
v3.1.2
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d *Newtonsoft.Json.dll
|
||||
9f1a8f06c284a4ee01f704d89003ddc7061846f2008094071e9adf08267849f9 *Newtonsoft.Json.Schema.dll
|
||||
d11b660612ce821ec03772b73aa3b8884a0479275c70085c7e143913a41a2d28 *Scoop.Validator.dll
|
||||
7496d5349a123a6e3696085662b2ff17b156ccdb0e30e0c396ac72d2da36ce1c *Newtonsoft.Json.Schema.dll
|
||||
83b1006443e8c340ca4c631614fc2ce0d5cb9a28c851e3b59724299f58b1397f *Scoop.Validator.dll
|
||||
87f8f8db2202a3fbef6f431d0b7e20cec9d32095c441927402041f3c4076c1b6 *validator.exe
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
56eb7f070929b239642dab729537dde2c2287bdb852ad9e80b5358c74b14bc2b2dded910d0e3b6304ea27eb587e5f19db0a92e1cbae6a70fb20b4ef05057e4ac *Newtonsoft.Json.dll
|
||||
551e772fe2ee72b349d5c4ed5d5f8d8957d50cfcbbde7af5d5740d9652bcad626a2c00bc0d9223db7c874962187a90f9160397f243eadee1c594585ba2b155e0 *Newtonsoft.Json.Schema.dll
|
||||
0a31d192c82bbd8ce50fb75dd5fe813c98bb870d54c112c600ae2e2436063cb2bd94bb206675dfe31ce89922e9a04a3d520ed579ab7198835190b67a6321a74e *Scoop.Validator.dll
|
||||
78b12beb1e67ac4f6efa0fcba57b4b34ea6a31d8b369934d6b6a6617386ef9939ea453ac262916e5857ce0359eb809424ea33c676a87a8fdfd77a59b2ce96db0 *Newtonsoft.Json.Schema.dll
|
||||
e9da4370aee4df47eedcf15d9749712eee513e5a9115b808617ddfcfde5bc47a0410edfb57508fcf51033c0be967611b2fd2c2ba944de7290c020cc67f77ac57 *Scoop.Validator.dll
|
||||
58a0c37e98cac17822c7756bf6686a5fb74e711b8d986d13bd2f689f6b3b1f485fcd908d92cbc6a162a0e5974c2c5a43de57d15f1996be0aa405e41ec2ec8393 *validator.exe
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net45" />
|
||||
<package id="Newtonsoft.Json.Schema" version="3.0.15" targetFramework="net45" />
|
||||
<package id="Microsoft.Net.Compilers.Toolset" version="4.9.2" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Newtonsoft.Json.Schema" version="4.0.1" targetFramework="net45" />
|
||||
<package id="Microsoft.Net.Compilers.Toolset" version="4.10.0" targetFramework="net45"
|
||||
developmentDependency="true" />
|
||||
</packages>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import
|
||||
Project="packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props"
|
||||
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')" />
|
||||
Project="packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props"
|
||||
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
@@ -23,9 +23,8 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference
|
||||
Include="Newtonsoft.Json.Schema, Version=3.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||
<HintPath>
|
||||
packages\Newtonsoft.Json.Schema.3.0.15\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
|
||||
Include="Newtonsoft.Json.Schema, Version=4.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||
<HintPath>packages\Newtonsoft.Json.Schema.4.0.1\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
@@ -52,7 +51,7 @@
|
||||
http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error
|
||||
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')"
|
||||
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props'))" />
|
||||
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')"
|
||||
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
BeforeAll {
|
||||
. "$PSScriptRoot\Scoop-TestLib.ps1"
|
||||
. "$PSScriptRoot\..\lib\core.ps1"
|
||||
. "$PSScriptRoot\..\lib\help.ps1"
|
||||
. "$PSScriptRoot\..\libexec\scoop-alias.ps1" | Out-Null
|
||||
. "$PSScriptRoot\..\lib\commands.ps1"
|
||||
}
|
||||
|
||||
Describe 'Manipulate Alias' -Tag 'Scoop' {
|
||||
@@ -15,32 +14,32 @@ Describe 'Manipulate Alias' -Tag 'Scoop' {
|
||||
ensure $shimdir
|
||||
}
|
||||
|
||||
It 'Creates a new alias if alias doesn''t exist' {
|
||||
$alias_file = "$shimdir\scoop-rm.ps1"
|
||||
$alias_file | Should -Not -Exist
|
||||
It 'Creates a new alias if it does not exist' {
|
||||
$alias_script = "$shimdir\scoop-rm.ps1"
|
||||
$alias_script | Should -Not -Exist
|
||||
|
||||
add_alias 'rm' '"hello, world!"'
|
||||
& $alias_file | Should -Be 'hello, world!'
|
||||
& $alias_script | Should -Be 'hello, world!'
|
||||
}
|
||||
|
||||
It 'Does not change existing file if its filename same as alias name' {
|
||||
$alias_file = "$shimdir\scoop-rm.ps1"
|
||||
It 'Skips an existing alias' {
|
||||
$alias_script = "$shimdir\scoop-rm.ps1"
|
||||
Mock abort {}
|
||||
New-Item $alias_file -Type File -Force
|
||||
$alias_file | Should -Exist
|
||||
New-Item $alias_script -Type File -Force
|
||||
$alias_script | Should -Exist
|
||||
|
||||
add_alias 'rm' '"test"'
|
||||
Should -Invoke -CommandName abort -Times 1 -ParameterFilter { $msg -eq "File 'scoop-rm.ps1' already exists in shims directory." }
|
||||
}
|
||||
|
||||
It 'Removes an existing alias' {
|
||||
$alias_file = "$shimdir\scoop-rm.ps1"
|
||||
$alias_file | Should -Exist
|
||||
$alias_script = "$shimdir\scoop-rm.ps1"
|
||||
$alias_script | Should -Exist
|
||||
Mock get_config { @(@{'rm' = 'scoop-rm' }) }
|
||||
Mock info {}
|
||||
|
||||
rm_alias 'rm'
|
||||
$alias_file | Should -Not -Exist
|
||||
$alias_script | Should -Not -Exist
|
||||
Should -Invoke -CommandName info -Times 1 -ParameterFilter { $msg -eq "Removing alias 'rm'..." }
|
||||
}
|
||||
}
|
||||
@@ -73,28 +73,6 @@ Describe 'Test-HelperInstalled' -Tag 'Scoop' {
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
|
||||
It 'should return true if aria2 is installed' {
|
||||
Mock Test-HelperInstalled { $true }
|
||||
Mock get_config { $true }
|
||||
Test-Aria2Enabled | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'should return false if aria2 is not installed' {
|
||||
Mock Test-HelperInstalled { $false }
|
||||
Mock get_config { $false }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
|
||||
Mock Test-HelperInstalled { $false }
|
||||
Mock get_config { $true }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
|
||||
Mock Test-HelperInstalled { $true }
|
||||
Mock get_config { $false }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Test-CommandAvailable' -Tag 'Scoop' {
|
||||
It 'should return true if command exists' {
|
||||
Test-CommandAvailable 'Write-Host' | Should -BeTrue
|
||||
@@ -259,6 +237,23 @@ Describe 'get_app_name_from_shim' -Tag 'Scoop', 'Windows' {
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'cache_path' -Tag 'Scoop' {
|
||||
It 'returns the correct cache path for a given input' {
|
||||
$url = 'https://example.com/git.zip'
|
||||
$ret = cache_path 'git' '2.44.0' $url
|
||||
$inputStream = [System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($url))
|
||||
$sha = (Get-FileHash -Algorithm SHA256 -InputStream $inputStream).Hash.ToLower().Substring(0, 7)
|
||||
$ret | Should -Be "$cachedir\git#2.44.0#$sha.zip"
|
||||
}
|
||||
|
||||
# # NOTE: Remove this 6 months after the feature ships.
|
||||
It 'returns the old format cache path for a given input' {
|
||||
Mock Test-Path { $true }
|
||||
$ret = cache_path 'git' '2.44.0' 'https://example.com/git.zip'
|
||||
$ret | Should -Be "$cachedir\git#2.44.0#https_example.com_git.zip"
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'sanitary_path' -Tag 'Scoop' {
|
||||
It 'removes invalid path characters from a string' {
|
||||
$path = 'test?.json'
|
||||
|
||||
@@ -14,7 +14,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
|
||||
function test_extract($extract_fn, $from, $removal) {
|
||||
$to = (strip_ext $from) -replace '\.tar$', ''
|
||||
& $extract_fn ($from -replace '/', '\') ($to -replace '/', '\') -Removal:$removal
|
||||
& $extract_fn ($from -replace '/', '\') ($to -replace '/', '\') -Removal:$removal -ExtractDir $args[0]
|
||||
return $to
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
}
|
||||
It 'Test cases should exist and hash should match' {
|
||||
$testcases | Should -Exist
|
||||
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be '791bfce192917a2ff225dcdd87d23ae5f720b20178d85e68e4b1b56139cf8e6a'
|
||||
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be '591072faabd419b77932b7023e5899b4e05c0bf8e6859ad367398e6bfe1eb203'
|
||||
}
|
||||
It 'Test cases should be extracted correctly' {
|
||||
{ Microsoft.PowerShell.Archive\Expand-Archive -Path $testcases -DestinationPath $working_dir } | Should -Not -Throw
|
||||
@@ -50,15 +50,42 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
$test6_1 = "$working_dir\7ZipTest6.part01.rar"
|
||||
$test6_2 = "$working_dir\7ZipTest6.part02.rar"
|
||||
$test6_3 = "$working_dir\7ZipTest6.part03.rar"
|
||||
$test7 = "$working_dir\NSISTest.exe"
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Item -Path $to -Recurse -Force
|
||||
}
|
||||
|
||||
It 'extract normal compressed file' {
|
||||
$to = test_extract 'Expand-7zipArchive' $test1
|
||||
$to | Should -Exist
|
||||
"$to\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 4
|
||||
}
|
||||
|
||||
It 'extract "extract_dir" correctly' {
|
||||
$to = test_extract 'Expand-7zipArchive' $test1 $false 'tmp'
|
||||
$to | Should -Exist
|
||||
"$to\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract "extract_dir" with spaces correctly' {
|
||||
$to = test_extract 'Expand-7zipArchive' $test1 $false 'tmp 2'
|
||||
$to | Should -Exist
|
||||
"$to\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract "extract_dir" with nested folder with same name' {
|
||||
$to = test_extract 'Expand-7zipArchive' $test1 $false 'keep\sub'
|
||||
$to | Should -Exist
|
||||
"$to\keep\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
(Get-ChildItem "$to\keep").Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract nested compressed file' {
|
||||
# file ext: tgz
|
||||
$to = test_extract 'Expand-7zipArchive' $test2
|
||||
@@ -94,62 +121,45 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract NSIS installer' {
|
||||
$to = test_extract 'Expand-7zipArchive' $test7
|
||||
$to | Should -Exist
|
||||
"$to\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'self-extract NSIS installer' {
|
||||
$to = "$working_dir\NSIS Test"
|
||||
$null = Invoke-ExternalCommand -FilePath $test7 -ArgumentList @('/S', '/NCRC', "/D=$to")
|
||||
$to | Should -Exist
|
||||
"$to\empty" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'works with "-Removal" switch ($removal param)' {
|
||||
$test1 | Should -Exist
|
||||
test_extract 'Expand-7zipArchive' $test1 $true
|
||||
$to = test_extract 'Expand-7zipArchive' $test1 $true
|
||||
$to | Should -Exist
|
||||
$test1 | Should -Not -Exist
|
||||
$test5_1 | Should -Exist
|
||||
$test5_2 | Should -Exist
|
||||
$test5_3 | Should -Exist
|
||||
test_extract 'Expand-7zipArchive' $test5_1 $true
|
||||
$to = test_extract 'Expand-7zipArchive' $test5_1 $true
|
||||
$to | Should -Exist
|
||||
$test5_1 | Should -Not -Exist
|
||||
$test5_2 | Should -Not -Exist
|
||||
$test5_3 | Should -Not -Exist
|
||||
$test6_1 | Should -Exist
|
||||
$test6_2 | Should -Exist
|
||||
$test6_3 | Should -Exist
|
||||
test_extract 'Expand-7zipArchive' $test6_1 $true
|
||||
$to = test_extract 'Expand-7zipArchive' $test6_1 $true
|
||||
$to | Should -Exist
|
||||
$test6_1 | Should -Not -Exist
|
||||
$test6_2 | Should -Not -Exist
|
||||
$test6_3 | Should -Not -Exist
|
||||
}
|
||||
}
|
||||
|
||||
Context 'zstd extraction' {
|
||||
|
||||
BeforeAll {
|
||||
if ($env:CI) {
|
||||
Mock Get-AppFilePath { $env:SCOOP_ZSTD_PATH } -ParameterFilter { $Helper -eq 'zstd' }
|
||||
Mock Get-AppFilePath { '7z.exe' } -ParameterFilter { $Helper -eq '7zip' }
|
||||
} elseif (!(installed zstd)) {
|
||||
scoop install zstd
|
||||
}
|
||||
|
||||
$test1 = "$working_dir\ZstdTest.zst"
|
||||
$test2 = "$working_dir\ZstdTest.tar.zst"
|
||||
}
|
||||
|
||||
It 'extract normal compressed file' {
|
||||
$to = test_extract 'Expand-ZstdArchive' $test1
|
||||
$to | Should -Exist
|
||||
"$to\ZstdTest" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract nested compressed file' {
|
||||
$to = test_extract 'Expand-ZstdArchive' $test2
|
||||
$to | Should -Exist
|
||||
"$to\ZstdTest" | Should -Exist
|
||||
(Get-ChildItem $to).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'works with "-Removal" switch ($removal param)' {
|
||||
$test1 | Should -Exist
|
||||
test_extract 'Expand-ZstdArchive' $test1 $true
|
||||
$test1 | Should -Not -Exist
|
||||
}
|
||||
}
|
||||
|
||||
Context 'msi extraction' {
|
||||
|
||||
BeforeAll {
|
||||
@@ -158,11 +168,13 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
} elseif (!(installed lessmsi)) {
|
||||
scoop install lessmsi
|
||||
}
|
||||
Copy-Item "$working_dir\MSITest.msi" "$working_dir\MSI Test.msi"
|
||||
$test1 = "$working_dir\MSITest.msi"
|
||||
$test2 = "$working_dir\MSITestNull.msi"
|
||||
$test2 = "$working_dir\MSI Test.msi"
|
||||
$test3 = "$working_dir\MSITestNull.msi"
|
||||
}
|
||||
|
||||
It 'extract normal MSI file' {
|
||||
It 'extract normal MSI file using msiexec' {
|
||||
Mock get_config { $false }
|
||||
$to = test_extract 'Expand-MsiArchive' $test1
|
||||
$to | Should -Exist
|
||||
@@ -170,12 +182,32 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
|
||||
(Get-ChildItem "$to\MSITest").Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract empty MSI file using lessmsi' {
|
||||
It 'extract normal MSI file with whitespace in path using msiexec' {
|
||||
Mock get_config { $false }
|
||||
$to = test_extract 'Expand-MsiArchive' $test2
|
||||
$to | Should -Exist
|
||||
"$to\MSITest\empty" | Should -Exist
|
||||
(Get-ChildItem "$to\MSITest").Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'extract normal MSI file using lessmsi' {
|
||||
Mock get_config { $true }
|
||||
$to = test_extract 'Expand-MsiArchive' $test1
|
||||
$to | Should -Exist
|
||||
}
|
||||
|
||||
It 'extract normal MSI file with whitespace in path using lessmsi' {
|
||||
Mock get_config { $true }
|
||||
$to = test_extract 'Expand-MsiArchive' $test2
|
||||
$to | Should -Exist
|
||||
}
|
||||
|
||||
It 'extract empty MSI file using lessmsi' {
|
||||
Mock get_config { $true }
|
||||
$to = test_extract 'Expand-MsiArchive' $test3
|
||||
$to | Should -Exist
|
||||
}
|
||||
|
||||
It 'works with "-Removal" switch ($removal param)' {
|
||||
Mock get_config { $false }
|
||||
$test1 | Should -Exist
|
||||
|
||||
@@ -14,11 +14,6 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
|
||||
Test-7zipRequirement -Uri 'test.bin' | Should -BeFalse
|
||||
Test-7zipRequirement -Uri @('test.xz', 'test.bin') | Should -BeTrue
|
||||
}
|
||||
It 'Test Zstd requirement' {
|
||||
Test-ZstdRequirement -Uri 'test.zst' | Should -BeTrue
|
||||
Test-ZstdRequirement -Uri 'test.bin' | Should -BeFalse
|
||||
Test-ZstdRequirement -Uri @('test.zst', 'test.bin') | Should -BeTrue
|
||||
}
|
||||
It 'Test lessmsi requirement' {
|
||||
Mock get_config { $true }
|
||||
Test-LessmsiRequirement -Uri 'test.msi' | Should -BeTrue
|
||||
@@ -27,7 +22,6 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
|
||||
}
|
||||
It 'Allow $Uri be $null' {
|
||||
Test-7zipRequirement -Uri $null | Should -BeFalse
|
||||
Test-ZstdRequirement -Uri $null | Should -BeFalse
|
||||
Test-LessmsiRequirement -Uri $null | Should -BeFalse
|
||||
}
|
||||
}
|
||||
@@ -71,6 +65,7 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
|
||||
BeforeAll {
|
||||
Mock Test-HelperInstalled { $false }
|
||||
Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' }
|
||||
Mock get_config { $false } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' }
|
||||
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' }
|
||||
|
||||
49
test/Scoop-Download.Tests.ps1
Normal file
49
test/Scoop-Download.Tests.ps1
Normal file
@@ -0,0 +1,49 @@
|
||||
BeforeAll {
|
||||
. "$PSScriptRoot\Scoop-TestLib.ps1"
|
||||
. "$PSScriptRoot\..\lib\core.ps1"
|
||||
. "$PSScriptRoot\..\lib\download.ps1"
|
||||
}
|
||||
|
||||
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
|
||||
It 'should return true if aria2 is installed' {
|
||||
Mock Test-HelperInstalled { $true }
|
||||
Mock get_config { $true }
|
||||
Test-Aria2Enabled | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'should return false if aria2 is not installed' {
|
||||
Mock Test-HelperInstalled { $false }
|
||||
Mock get_config { $false }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
|
||||
Mock Test-HelperInstalled { $false }
|
||||
Mock get_config { $true }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
|
||||
Mock Test-HelperInstalled { $true }
|
||||
Mock get_config { $false }
|
||||
Test-Aria2Enabled | Should -BeFalse
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'url_filename' -Tag 'Scoop' {
|
||||
It 'should extract the real filename from an url' {
|
||||
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||
}
|
||||
|
||||
It 'can be tricked with a hash to override the real filename' {
|
||||
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'url_remote_filename' -Tag 'Scoop' {
|
||||
It 'should extract the real filename from an url' {
|
||||
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||
}
|
||||
|
||||
It 'can not be tricked with a hash to override the real filename' {
|
||||
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
|
||||
}
|
||||
}
|
||||
@@ -12,28 +12,6 @@ Describe 'appname_from_url' -Tag 'Scoop' {
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'url_filename' -Tag 'Scoop' {
|
||||
It 'should extract the real filename from an url' {
|
||||
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||
}
|
||||
|
||||
It 'can be tricked with a hash to override the real filename' {
|
||||
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'url_remote_filename' -Tag 'Scoop' {
|
||||
It 'should extract the real filename from an url' {
|
||||
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||
}
|
||||
|
||||
It 'can not be tricked with a hash to override the real filename' {
|
||||
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'is_in_dir' -Tag 'Scoop', 'Windows' {
|
||||
It 'should work correctly' {
|
||||
is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
BeforeAll {
|
||||
. "$PSScriptRoot\..\lib\json.ps1"
|
||||
. "$PSScriptRoot\..\lib\core.ps1"
|
||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||
. "$PSScriptRoot\..\lib\buckets.ps1"
|
||||
. "$PSScriptRoot\..\lib\database.ps1"
|
||||
. "$PSScriptRoot\..\lib\autoupdate.ps1"
|
||||
}
|
||||
|
||||
Describe 'JSON parse and beautify' -Tag 'Scoop' {
|
||||
Context 'Parse JSON' {
|
||||
It 'success with valid json' {
|
||||
{ parse_json "$PSScriptRoot\fixtures\manifest\wget.json" } | Should -Not -Throw
|
||||
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\wget.json"
|
||||
$parsed | Should -Not -Be $null
|
||||
}
|
||||
It 'fails with invalid json' {
|
||||
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Throw
|
||||
It 'returns null and warns with invalid json' {
|
||||
Mock warn {}
|
||||
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Not -Throw
|
||||
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json"
|
||||
$parsed | Should -Be $null
|
||||
Should -Invoke -CommandName warn -Times 1
|
||||
}
|
||||
}
|
||||
Context 'Beautify JSON' {
|
||||
@@ -84,3 +94,144 @@ Describe 'Manifest Validator' -Tag 'Validator' {
|
||||
$validator.Errors | Select-Object -Last 1 | Should -Match 'Required properties are missing from object: version\.'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-RelativePathCompat' -Tag 'Scoop' {
|
||||
It 'returns relative path for child path' {
|
||||
$from = 'C:\root\bucket'
|
||||
$to = 'C:\root\bucket\foo\bar.json'
|
||||
Get-RelativePathCompat $from $to | Should -Be 'foo\bar.json'
|
||||
}
|
||||
It 'returns original when different drive/scheme' {
|
||||
$from = 'C:\root\bucket'
|
||||
$to = 'D:\other\file.json'
|
||||
Get-RelativePathCompat $from $to | Should -Be $to
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Find-HistoricalManifestInCache' -Tag 'Scoop' {
|
||||
It 'returns $null when sqlite cache disabled' {
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
|
||||
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.0.0'
|
||||
$result | Should -Be $null
|
||||
}
|
||||
|
||||
It 'returns manifest text and version when cache has exact match' {
|
||||
$tempUM = Join-Path $env:TEMP 'ScoopTestsUM'
|
||||
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
|
||||
Mock Get-ScoopDBItem {
|
||||
$dt = New-Object System.Data.DataTable
|
||||
[void]$dt.Columns.Add('manifest')
|
||||
$row = $dt.NewRow()
|
||||
$row['manifest'] = '{"version":"1.2.3"}'
|
||||
[void]$dt.Rows.Add($row)
|
||||
Write-Output $dt -NoEnumerate
|
||||
}
|
||||
Mock ensure {}
|
||||
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.2.3'
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
$result.version | Should -Be '1.2.3'
|
||||
$result.ManifestText | Should -Match '"version":"1.2.3"'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Find-HistoricalManifestInGit' -Tag 'Scoop' {
|
||||
It 'returns $null when git history search disabled' {
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $false }
|
||||
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
|
||||
$result | Should -Be $null
|
||||
}
|
||||
|
||||
It 'returns manifest text on version match' {
|
||||
$bucketRoot = 'C:\b\root'
|
||||
$innerBucket = 'C:\b\root\bucket'
|
||||
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $true }
|
||||
Mock Find-BucketDirectory -ParameterFilter { $Root } { $bucketRoot }
|
||||
Mock Find-BucketDirectory -ParameterFilter { -not $Root } { $innerBucket }
|
||||
Mock Test-Path -ParameterFilter { $Path -eq (Join-Path $bucketRoot '.git') } { $true }
|
||||
Mock Test-Path -ParameterFilter { $Path -eq $innerBucket -and $PathType -eq 'Container' } { $true }
|
||||
# Behavior-oriented mocks: using HEAD should yield a wrong version
|
||||
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' -and $ArgumentList[1] -like 'HEAD*' } { $global:LASTEXITCODE = 0; return '{"version":"2.0.0"}' }
|
||||
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' } { $global:LASTEXITCODE = 0; return '{"version":"1.0.0"}' }
|
||||
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'log' } { @('abcdef0123456789') }
|
||||
|
||||
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
|
||||
$result | Should -Not -BeNullOrEmpty
|
||||
$result.version | Should -Be '1.0.0'
|
||||
$result.ManifestText | Should -Match '"version":"1.0.0"'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'generate_user_manifest (history-aware)' -Tag 'Scoop' {
|
||||
It 'returns manifest_path when versions match' {
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='1.0.0' }, 'main', $null }
|
||||
Mock manifest_path { 'C:\path\foo.json' }
|
||||
$p = generate_user_manifest 'foo' 'main' '1.0.0'
|
||||
$p | Should -Be 'C:\path\foo.json'
|
||||
}
|
||||
|
||||
It 'prefers history orchestrator hit (cache) when enabled' {
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
|
||||
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
|
||||
Mock Find-HistoricalManifest { @{ path = 'C:\cache\foo.json'; version = '1.0.0'; source='sqlite_exact_match' } }
|
||||
|
||||
Mock info {}
|
||||
Mock warn {}
|
||||
$p = generate_user_manifest 'foo' 'main' '1.0.0'
|
||||
$p | Should -Be 'C:\cache\foo.json'
|
||||
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
|
||||
|
||||
}
|
||||
|
||||
It 'falls back to git history when cache misses' {
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
|
||||
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
|
||||
Mock Find-HistoricalManifest { @{ path = 'C:\git\foo.json'; version = '1.0.0'; source='git_manifest:hash' } }
|
||||
Mock info {}
|
||||
Mock warn {}
|
||||
$p = generate_user_manifest 'foo' 'main' '1.0.0'
|
||||
$p | Should -Be 'C:\git\foo.json'
|
||||
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
|
||||
|
||||
}
|
||||
|
||||
It 'uses autoupdate when no history found and autoupdate exists' {
|
||||
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
|
||||
Mock Find-HistoricalManifest { $null }
|
||||
|
||||
Mock ensure {}
|
||||
Mock usermanifestsdir { $umdir }
|
||||
Mock Invoke-AutoUpdate {}
|
||||
$p = generate_user_manifest 'foo' 'main' '1.0.0'
|
||||
$p | Should -Be (Join-Path $umdir 'foo.json')
|
||||
}
|
||||
|
||||
It 'on autoupdate failure aborts with concise message' {
|
||||
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
|
||||
Mock Find-HistoricalManifest { $null }
|
||||
Mock ensure {}
|
||||
Mock usermanifestsdir { $umdir }
|
||||
Mock Invoke-AutoUpdate { throw 'fail' }
|
||||
Mock warn {}
|
||||
Mock info {}
|
||||
Mock Write-Host {}
|
||||
Mock abort { throw 'aborted' }
|
||||
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
|
||||
}
|
||||
|
||||
It 'aborts when no history and no autoupdate' {
|
||||
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
|
||||
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
|
||||
Mock Find-HistoricalManifest { $null }
|
||||
|
||||
Mock warn {}
|
||||
Mock info {}
|
||||
Mock abort { throw 'aborted' }
|
||||
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# copies fixtures to a working directory
|
||||
function setup_working($name) {
|
||||
$fixtures = "$PSScriptRoot/fixtures/$name"
|
||||
$fixtures = "$PSScriptRoot\fixtures\$name"
|
||||
if (!(Test-Path $fixtures)) {
|
||||
Write-Host "couldn't find fixtures for $name at $fixtures" -f red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# reset working dir
|
||||
$working_dir = "$([IO.Path]::GetTempPath())ScoopTestFixtures/$name"
|
||||
$working_dir = "$([IO.Path]::GetTempPath())ScoopTestFixtures\$name"
|
||||
|
||||
if (Test-Path $working_dir) {
|
||||
Remove-Item -Recurse -Force $working_dir
|
||||
|
||||
@@ -70,19 +70,6 @@ if ($env:CI -eq $true) {
|
||||
Invoke-WebRequest -Uri $source -OutFile $destination
|
||||
& 7z.exe x "$env:SCOOP_HELPERS_PATH\innounp.rar" -o"$env:SCOOP_HELPERS_PATH\innounp" -y | Out-Null
|
||||
}
|
||||
|
||||
# Only download zstd for AppVeyor, GitHub Actions has zstd installed by default
|
||||
if ($env:BHBuildSystem -eq 'AppVeyor') {
|
||||
$env:SCOOP_ZSTD_PATH = "$env:SCOOP_HELPERS_PATH\zstd\zstd.exe"
|
||||
if (!(Test-Path $env:SCOOP_ZSTD_PATH)) {
|
||||
$source = 'https://github.com/facebook/zstd/releases/download/v1.5.1/zstd-v1.5.1-win32.zip'
|
||||
$destination = "$env:SCOOP_HELPERS_PATH\zstd.zip"
|
||||
Invoke-WebRequest -Uri $source -OutFile $destination
|
||||
& 7z.exe x "$env:SCOOP_HELPERS_PATH\zstd.zip" -o"$env:SCOOP_HELPERS_PATH\zstd" -y | Out-Null
|
||||
}
|
||||
} else {
|
||||
$env:SCOOP_ZSTD_PATH = (Get-Command zstd.exe).Path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
test/fixtures/decompress/TestCases.zip
vendored
BIN
test/fixtures/decompress/TestCases.zip
vendored
Binary file not shown.
Reference in New Issue
Block a user