├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── PSScriptAnalyzerSettings.psd1 ├── README.md ├── appveyor.yml ├── bin ├── auto-pr.ps1 ├── checkhashes.ps1 ├── checkurls.ps1 ├── checkver.ps1 ├── describe.ps1 ├── formatjson.ps1 ├── install.ps1 ├── missing-checkver.ps1 ├── refresh.ps1 ├── scoop.ps1 ├── test.ps1 └── uninstall.ps1 ├── buckets.json ├── lib ├── autoupdate.ps1 ├── buckets.ps1 ├── commands.ps1 ├── core.ps1 ├── database.ps1 ├── decompress.ps1 ├── depends.ps1 ├── description.ps1 ├── diagnostic.ps1 ├── getopt.ps1 ├── help.ps1 ├── install.ps1 ├── json.ps1 ├── manifest.ps1 ├── psmodules.ps1 ├── shortcuts.ps1 ├── system.ps1 └── versions.ps1 ├── libexec ├── scoop-alias.ps1 ├── scoop-bucket.ps1 ├── scoop-cache.ps1 ├── scoop-cat.ps1 ├── scoop-checkup.ps1 ├── scoop-cleanup.ps1 ├── scoop-config.ps1 ├── scoop-create.ps1 ├── scoop-depends.ps1 ├── scoop-download.ps1 ├── scoop-export.ps1 ├── scoop-help.ps1 ├── scoop-hold.ps1 ├── scoop-home.ps1 ├── scoop-import.ps1 ├── scoop-info.ps1 ├── scoop-install.ps1 ├── scoop-list.ps1 ├── scoop-prefix.ps1 ├── scoop-reset.ps1 ├── scoop-search.ps1 ├── scoop-shim.ps1 ├── scoop-status.ps1 ├── scoop-unhold.ps1 ├── scoop-uninstall.ps1 ├── scoop-update.ps1 ├── scoop-virustotal.ps1 └── scoop-which.ps1 ├── schema.json ├── supporting ├── formats │ └── ScoopTypes.Format.ps1xml ├── shims │ ├── 71 │ │ ├── checksum.sha256 │ │ ├── checksum.sha512 │ │ └── shim.exe │ ├── kiennq │ │ ├── checksum.sha256 │ │ ├── checksum.sha512 │ │ ├── shim.exe │ │ └── version.txt │ └── scoopcs │ │ ├── checksum.sha256 │ │ ├── checksum.sha512 │ │ ├── shim.exe │ │ └── version.txt └── validator │ ├── .gitignore │ ├── Scoop.Validator.cs │ ├── bin │ ├── Newtonsoft.Json.Schema.dll │ ├── Newtonsoft.Json.dll │ ├── Scoop.Validator.dll │ ├── checksum.sha256 │ ├── checksum.sha512 │ └── validator.exe │ ├── build.ps1 │ ├── install.ps1 │ ├── packages.config │ ├── update.ps1 │ ├── validator.cs │ └── validator.csproj └── test ├── Import-Bucket-Tests.ps1 ├── Scoop-00File.Tests.ps1 ├── Scoop-00Linting.Tests.ps1 ├── Scoop-Commands.Tests.ps1 ├── Scoop-Config.Tests.ps1 ├── Scoop-Core.Tests.ps1 ├── Scoop-Decompress.Tests.ps1 ├── Scoop-Depends.Tests.ps1 ├── Scoop-GetOpts.Tests.ps1 ├── Scoop-Install.Tests.ps1 ├── Scoop-Manifest.Tests.ps1 ├── Scoop-TestLib.ps1 ├── Scoop-Versions.Tests.ps1 ├── bin ├── init.ps1 └── test.ps1 └── fixtures ├── decompress └── TestCases.zip ├── format ├── formatted │ ├── 1-easy.json │ ├── 2-whitespaces-mess.json │ ├── 3-array-with-single-and-multi.json │ └── 4-script-block.json └── unformatted │ ├── 1-easy.json │ ├── 2-whitespaces-mess.json │ ├── 3-array-with-single-and-multi.json │ └── 4-script-block.json ├── is_directory ├── i_am_a_directory │ └── .gitkeep └── i_am_a_file.txt ├── manifest ├── broken_schema.json ├── broken_wget.json ├── invalid_wget.json └── wget.json ├── movedir ├── user with 'quote │ └── _tmp │ │ ├── subdir │ │ └── test.txt │ │ └── test.txt ├── user with space │ └── _tmp │ │ ├── subdir │ │ └── test.txt │ │ └── test.txt └── user │ └── _tmp │ ├── subdir │ └── test.txt │ └── test.txt └── shim ├── shim-test.ps1 └── user with 'quote └── shim-test.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig (is awesome): http://EditorConfig.org 2 | 3 | # * top-most EditorConfig file 4 | root = true 5 | 6 | # default style settings 7 | [*] 8 | charset = utf-8 9 | end_of_line = crlf 10 | indent_size = 4 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.{[Bb][Aa][Tt],[Cc][Mm][Dd]}] 16 | # DOS/Win *requires* BAT/CMD files to have CRLF newlines 17 | end_of_line = crlf 18 | 19 | [*.{yml, yaml}] 20 | indent_size = 2 21 | 22 | # Makefiles require tab indentation 23 | [{{M,m,GNU}akefile{,.*},*.mak,*.mk}] 24 | indent_style = tab 25 | end_of_line = lf 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # retain windows line-endings in case checked out on mac or linux 2 | * text eol=crlf 3 | *.exe -text 4 | *.zip -text 5 | *.dll -text 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug Report" 3 | about: "I am facing some problems." 4 | title: '[Bug] ' 5 | labels: "bug" 6 | 7 | --- 8 | 9 | 17 | 18 | ## Bug Report 19 | 20 | #### Current Behavior 21 | 22 | 23 | #### Expected Behavior 24 | 25 | 26 | #### Additional context/output 27 | 28 | 29 | #### Possible Solution 30 | 31 | 32 | ### System details 33 | 34 | **Windows version:** [e.g. 7, 8, 10, 11] 35 | 36 | **OS architecture:** [e.g. 32bit, 64bit, arm64] 37 | 38 | **PowerShell version:** [output of `"$($PSVersionTable.PSVersion)"`] 39 | 40 | **Additional software:** [(optional) e.g. ConEmu, Git] 41 | 42 | #### Scoop Configuration 43 | 44 | 45 | ```json 46 | //# Your configuration here 47 | ``` 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature Request" 3 | about: "I have a suggestion (and may want to implement it)!" 4 | title: '[Feature] ' 5 | labels: "enhancement" 6 | 7 | --- 8 | 9 | 17 | 18 | ## Feature Request 19 | 20 | #### Is your feature request related to a problem? Please describe. 21 | 22 | 23 | #### Describe the solution you'd like 24 | 25 | 26 | #### Describe alternatives you've considered 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | #### Description 14 | 15 | 16 | #### Motivation and Context 17 | 18 | 19 | Closes #XXXX 20 | 21 | Relates to #XXXX 22 | 23 | #### How Has This Been Tested? 24 | 25 | 26 | 27 | 28 | #### Checklist: 29 | 30 | 31 | - [ ] I have read the [Contributing Guide](https://github.com/ScoopInstaller/.github/blob/main/.github/CONTRIBUTING.md). 32 | - [ ] I have ensured that I am targeting the `develop` branch. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] I have updated the tests accordingly. 35 | - [ ] I have added an entry in the CHANGELOG. 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ~/.github/dependabot.yml 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" # == /.github/workflows/ 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Scoop Core CI Tests 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test_powershell: 9 | name: WindowsPowerShell 10 | runs-on: windows-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@main 14 | with: 15 | fetch-depth: 2 16 | - name: Init Test Suite 17 | uses: potatoqualitee/psmodulecache@main 18 | with: 19 | modules-to-cache: BuildHelpers 20 | shell: powershell 21 | - name: Test Scoop Core 22 | shell: powershell 23 | run: ./test/bin/test.ps1 24 | test_pwsh: 25 | name: PowerShell 26 | runs-on: windows-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@main 30 | with: 31 | fetch-depth: 2 32 | - name: Init Test Suite 33 | uses: potatoqualitee/psmodulecache@main 34 | with: 35 | modules-to-cache: BuildHelpers 36 | shell: pwsh 37 | - name: Test Scoop Core 38 | shell: pwsh 39 | run: ./test/bin/test.ps1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | ._.DS_Store 4 | scoop.sublime-workspace 5 | test/installer/tmp/* 6 | test/tmp/* 7 | *~ 8 | TestResults.xml 9 | supporting/sqlite/* 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "ms-vscode.PowerShell" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Configure PSScriptAnalyzer settings 2 | { 3 | "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", 4 | "powershell.codeFormatting.preset": "OTBS", 5 | "powershell.codeFormatting.alignPropertyValuePairs": true, 6 | "powershell.codeFormatting.ignoreOneLineBlock": true, 7 | "powershell.codeFormatting.useConstantStrings": true, 8 | "powershell.codeFormatting.useCorrectCasing": true, 9 | "powershell.codeFormatting.whitespaceBetweenParameters": true, 10 | "files.exclude": { 11 | "**/.git": true, 12 | "**/.svn": true, 13 | "**/.hg": true, 14 | "**/CVS": true, 15 | "**/.DS_Store": true, 16 | "**/tmp": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: UNLICENSE or MIT 2 | 3 | INFORMATION ABOUT THIS PROJECT'S LICENSE (SHORT) 4 | ============================================================================================ 5 | This project is licensed under the Unlicense or the MIT license, 6 | at your option. 7 | 8 | INFORMATION ABOUT THIS PROJECT'S LICENSE (LONG) 9 | ============================================================================================ 10 | This project ("Scoop") is free software, licensed under the Unlicense or the 11 | MIT license, at your option. Scoop was previously licensed under only the Unlicense, 12 | but was dual-licensed from version 0.2.0. 13 | 14 | Scoop comes with ABSOLUTELY NO WARRANTY. Use it at your own risk. Scoop is provided 15 | on an AS-IS BASIS and its contributors disclaim all warranties. 16 | 17 | You may use, modify, distribute, sell, copy, compile, or merge Scoop by any means. 18 | 19 | Copies of both licenses can be found below. 20 | 21 | THE LICENSE OF SCOOP 22 | ============================================================================================ 23 | Unlicense 24 | --------- 25 | This is free and unencumbered software released into the public domain. 26 | 27 | Anyone is free to copy, modify, publish, use, compile, sell, or 28 | distribute this software, either in source code form or as a compiled 29 | binary, for any purpose, commercial or non-commercial, and by any 30 | means. 31 | 32 | In jurisdictions that recognize copyright laws, the author or authors 33 | of this software dedicate any and all copyright interest in the 34 | software to the public domain. We make this dedication for the benefit 35 | of the public at large and to the detriment of our heirs and 36 | successors. We intend this dedication to be an overt act of 37 | relinquishment in perpetuity of all present and future rights to this 38 | software under copyright law. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 43 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 44 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 45 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 46 | OTHER DEALINGS IN THE SOFTWARE. 47 | 48 | For more information, please refer to 49 | 50 | MIT license 51 | ----------- 52 | The MIT License (MIT) 53 | 54 | Copyright (c) 2013-2017 Luke Sampson (https://github.com/lukesampson) 55 | Copyright (c) 2013-present Scoop contributors (https://github.com/ScoopInstaller/Scoop/graphs/contributors) 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining a copy 58 | of this software and associated documentation files (the "Software"), to deal 59 | in the Software without restriction, including without limitation the rights 60 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | copies of the Software, and to permit persons to whom the Software is 62 | furnished to do so, subject to the following conditions: 63 | 64 | The above copyright notice and this permission notice shall be included in all 65 | copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 69 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 70 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 71 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 72 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 73 | SOFTWARE. 74 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Only diagnostic records of the specified severity will be generated. 3 | # Uncomment the following line if you only want Errors and Warnings but 4 | # not Information diagnostic records. 5 | Severity = @('Error') 6 | 7 | # Analyze **only** the following rules. Use IncludeRules when you want 8 | # to invoke only a small subset of the defualt rules. 9 | # IncludeRules = @('PSAvoidDefaultValueSwitchParameter', 10 | # 'PSMisleadingBacktick', 11 | # 'PSMissingModuleManifestField', 12 | # 'PSReservedCmdletChar', 13 | # 'PSReservedParams', 14 | # 'PSShouldProcess', 15 | # 'PSUseApprovedVerbs', 16 | # 'PSAvoidUsingCmdletAliases', 17 | # 'PSUseDeclaredVarsMoreThanAssignments') 18 | 19 | # Do not analyze the following rules. Use ExcludeRules when you have 20 | # commented out the IncludeRules settings above and want to include all 21 | # the default rules except for those you exclude below. 22 | # Note: if a rule is in both IncludeRules and ExcludeRules, the rule 23 | # will be excluded. 24 | ExcludeRules = @( 25 | # Currently Scoop widely uses Write-Host to output colored text. 26 | 'PSAvoidUsingWriteHost' 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}-{branch}" 2 | branches: 3 | except: 4 | - gh-pages 5 | build: false 6 | deploy: false 7 | clone_depth: 2 8 | image: Visual Studio 2022 9 | environment: 10 | matrix: 11 | - PowerShell: 5 12 | - PowerShell: 7 13 | matrix: 14 | fast_finish: true 15 | for: 16 | - matrix: 17 | only: 18 | - PowerShell: 5 19 | cache: 20 | - '%USERPROFILE%\Documents\WindowsPowerShell\Modules -> appveyor.yml, test\bin\*.ps1' 21 | - C:\projects\helpers -> appveyor.yml, test\bin\*.ps1 22 | install: 23 | - ps: .\test\bin\init.ps1 24 | test_script: 25 | - ps: .\test\bin\test.ps1 26 | - matrix: 27 | only: 28 | - PowerShell: 7 29 | cache: 30 | - '%USERPROFILE%\Documents\PowerShell\Modules -> appveyor.yml, test\bin\*.ps1' 31 | - C:\projects\helpers -> appveyor.yml, test\bin\*.ps1 32 | install: 33 | - pwsh: .\test\bin\init.ps1 34 | test_script: 35 | - pwsh: .\test\bin\test.ps1 36 | -------------------------------------------------------------------------------- /bin/checkhashes.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Check if ALL urls inside manifest have correct hashes. 4 | .PARAMETER App 5 | Manifest to be checked. 6 | Wildcard is supported. 7 | .PARAMETER Dir 8 | Where to search for manifest(s). 9 | .PARAMETER Update 10 | When there are mismatched hashes, manifest will be updated. 11 | .PARAMETER ForceUpdate 12 | Manifest will be updated all the time. Not only when there are mismatched hashes. 13 | .PARAMETER SkipCorrect 14 | Manifests without mismatch will not be shown. 15 | .PARAMETER UseCache 16 | Downloaded files will not be deleted after script finish. 17 | Should not be used, because check should be used for downloading actual version of file (as normal user, not finding in some document from vendors, which could be damaged / wrong (Example: Slack@3.3.1 ScoopInstaller/Extras#1192)), not some previously downloaded. 18 | .EXAMPLE 19 | PS BUCKETROOT> .\bin\checkhashes.ps1 20 | Check all manifests for hash mismatch. 21 | .EXAMPLE 22 | PS BUCKETROOT> .\bin\checkhashes.ps1 MANIFEST -Update 23 | Check MANIFEST and Update if there are some wrong hashes. 24 | #> 25 | param( 26 | [String] $App = '*', 27 | [Parameter(Mandatory = $true)] 28 | [ValidateScript( { 29 | if (!(Test-Path $_ -Type Container)) { 30 | throw "$_ is not a directory!" 31 | } else { 32 | $true 33 | } 34 | })] 35 | [String] $Dir, 36 | [Switch] $Update, 37 | [Switch] $ForceUpdate, 38 | [Switch] $SkipCorrect, 39 | [Alias('k')] 40 | [Switch] $UseCache 41 | ) 42 | 43 | . "$PSScriptRoot\..\lib\core.ps1" 44 | . "$PSScriptRoot\..\lib\manifest.ps1" 45 | . "$PSScriptRoot\..\lib\buckets.ps1" 46 | . "$PSScriptRoot\..\lib\autoupdate.ps1" 47 | . "$PSScriptRoot\..\lib\json.ps1" 48 | . "$PSScriptRoot\..\lib\versions.ps1" 49 | . "$PSScriptRoot\..\lib\install.ps1" 50 | 51 | $Dir = Convert-Path $Dir 52 | if ($ForceUpdate) { $Update = $true } 53 | # Cleanup 54 | if (!$UseCache) { Remove-Item "$cachedir\*HASH_CHECK*" -Force } 55 | 56 | function err ([String] $name, [String[]] $message) { 57 | Write-Host "$name`: " -ForegroundColor Red -NoNewline 58 | Write-Host ($message -join "`r`n") -ForegroundColor Red 59 | } 60 | 61 | $MANIFESTS = @() 62 | foreach ($single in Get-ChildItem $Dir -Filter "$App.json" -Recurse) { 63 | $name = $single.BaseName 64 | $file = $single.FullName 65 | $manifest = parse_json $file 66 | 67 | # Skip nighly manifests, since their hash validation is skipped 68 | if ($manifest.version -eq 'nightly') { continue } 69 | 70 | $urls = @() 71 | $hashes = @() 72 | 73 | if ($manifest.url) { 74 | $manifest.url | ForEach-Object { $urls += $_ } 75 | $manifest.hash | ForEach-Object { $hashes += $_ } 76 | } elseif ($manifest.architecture) { 77 | # First handle 64bit 78 | script:url $manifest '64bit' | ForEach-Object { $urls += $_ } 79 | hash $manifest '64bit' | ForEach-Object { $hashes += $_ } 80 | script:url $manifest '32bit' | ForEach-Object { $urls += $_ } 81 | hash $manifest '32bit' | ForEach-Object { $hashes += $_ } 82 | script:url $manifest 'arm64' | ForEach-Object { $urls += $_ } 83 | hash $manifest 'arm64' | ForEach-Object { $hashes += $_ } 84 | } else { 85 | err $name 'Manifest does not contain URL property.' 86 | continue 87 | } 88 | 89 | # Number of URLS and Hashes is different 90 | if ($urls.Length -ne $hashes.Length) { 91 | err $name 'URLS and hashes count mismatch.' 92 | continue 93 | } 94 | 95 | $MANIFESTS += @{ 96 | app = $name 97 | file = $file 98 | manifest = $manifest 99 | urls = $urls 100 | hashes = $hashes 101 | } 102 | } 103 | 104 | # clear any existing events 105 | Get-Event | ForEach-Object { Remove-Event $_.SourceIdentifier } 106 | 107 | foreach ($current in $MANIFESTS) { 108 | $count = 0 109 | # Array of indexes mismatched hashes. 110 | $mismatched = @() 111 | # Array of computed hashes 112 | $actuals = @() 113 | 114 | $current.urls | ForEach-Object { 115 | $algorithm, $expected = get_hash $current.hashes[$count] 116 | if ($UseCache) { 117 | $version = $current.manifest.version 118 | } else { 119 | $version = 'HASH_CHECK' 120 | } 121 | 122 | Invoke-CachedDownload $current.app $version $_ $null $null -use_cache:$UseCache 123 | 124 | $to_check = cache_path $current.app $version $_ 125 | $actual_hash = (Get-FileHash -Path $to_check -Algorithm $algorithm).Hash.ToLower() 126 | 127 | # Append type of algorithm to both expected and actual if it's not sha256 128 | if ($algorithm -ne 'sha256') { 129 | $actual_hash = "$algorithm`:$actual_hash" 130 | $expected = "$algorithm`:$expected" 131 | } 132 | 133 | $actuals += $actual_hash 134 | if ($actual_hash -ne $expected) { 135 | $mismatched += $count 136 | } 137 | $count++ 138 | } 139 | 140 | if ($mismatched.Length -eq 0 ) { 141 | if (!$SkipCorrect) { 142 | Write-Host "$($current.app): " -NoNewline 143 | Write-Host 'OK' -ForegroundColor Green 144 | } 145 | } else { 146 | Write-Host "$($current.app): " -NoNewline 147 | Write-Host 'Mismatch found ' -ForegroundColor Red 148 | $mismatched | ForEach-Object { 149 | $file = cache_path $current.app $version $current.urls[$_] 150 | Write-Host "`tURL:`t`t$($current.urls[$_])" 151 | if (Test-Path $file) { 152 | Write-Host "`tFirst bytes:`t$((get_magic_bytes_pretty $file ' ').ToUpper())" 153 | } 154 | Write-Host "`tExpected:`t$($current.hashes[$_])" -ForegroundColor Green 155 | Write-Host "`tActual:`t`t$($actuals[$_])" -ForegroundColor Red 156 | } 157 | } 158 | 159 | if ($Update) { 160 | if ($current.manifest.url -and $current.manifest.hash) { 161 | $current.manifest.hash = $actuals 162 | } else { 163 | $platforms = ($current.manifest.architecture | Get-Member -MemberType NoteProperty).Name 164 | # Defaults to zero, don't know, which architecture is available 165 | $64bit_count = 0 166 | $32bit_count = 0 167 | $arm64_count = 0 168 | 169 | # 64bit is get, donwloaded and added first 170 | if ($platforms.Contains('64bit')) { 171 | $64bit_count = $current.manifest.architecture.'64bit'.hash.Count 172 | $current.manifest.architecture.'64bit'.hash = $actuals[0..($64bit_count - 1)] 173 | } 174 | if ($platforms.Contains('32bit')) { 175 | $32bit_count = $current.manifest.architecture.'32bit'.hash.Count 176 | $current.manifest.architecture.'32bit'.hash = $actuals[($64bit_count)..($64bit_count + $32bit_count - 1)] 177 | } 178 | if ($platforms.Contains('arm64')) { 179 | $arm64_count = $current.manifest.architecture.'arm64'.hash.Count 180 | $current.manifest.architecture.'arm64'.hash = $actuals[($64bit_count + $32bit_count)..($64bit_count + $32bit_count + $arm64_count - 1)] 181 | } 182 | } 183 | 184 | Write-Host "Writing updated $($current.app) manifest" -ForegroundColor DarkGreen 185 | 186 | $current.manifest = $current.manifest | ConvertToPrettyJson 187 | $path = Convert-Path $current.file 188 | [System.IO.File]::WriteAllLines($path, $current.manifest) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /bin/checkurls.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | List manifests which do not have valid URLs. 4 | .PARAMETER App 5 | Manifest name to search. 6 | Placeholder is supported. 7 | .PARAMETER Dir 8 | Where to search for manifest(s). 9 | .PARAMETER Timeout 10 | How long (seconds) the request can be pending before it times out. 11 | .PARAMETER SkipValid 12 | Manifests will all valid URLs will not be shown. 13 | #> 14 | param( 15 | [String] $App = '*', 16 | [Parameter(Mandatory = $true)] 17 | [ValidateScript( { 18 | if (!(Test-Path $_ -Type Container)) { 19 | throw "$_ is not a directory!" 20 | } else { 21 | $true 22 | } 23 | })] 24 | [String] $Dir, 25 | [Int] $Timeout = 5, 26 | [Switch] $SkipValid 27 | ) 28 | 29 | . "$PSScriptRoot\..\lib\core.ps1" 30 | . "$PSScriptRoot\..\lib\manifest.ps1" 31 | . "$PSScriptRoot\..\lib\install.ps1" 32 | 33 | $Dir = Convert-Path $Dir 34 | $Queue = @() 35 | 36 | Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { 37 | $manifest = parse_json $_.FullName 38 | $Queue += , @($_.BaseName, $manifest) 39 | } 40 | 41 | Write-Host '[' -NoNewLine 42 | Write-Host 'U' -NoNewLine -ForegroundColor Cyan 43 | Write-Host ']RLs' 44 | Write-Host ' | [' -NoNewLine 45 | Write-Host 'O' -NoNewLine -ForegroundColor Green 46 | Write-Host ']kay' 47 | Write-Host ' | | [' -NoNewLine 48 | Write-Host 'F' -NoNewLine -ForegroundColor Red 49 | Write-Host ']ailed' 50 | Write-Host ' | | |' 51 | 52 | function test_dl([String] $url, $cookies) { 53 | # Trim renaming suffix, prevent getting 40x response 54 | $url = ($url -split '#/')[0] 55 | 56 | $wreq = [Net.WebRequest]::Create($url) 57 | $wreq.Timeout = $Timeout * 1000 58 | if ($wreq -is [Net.HttpWebRequest]) { 59 | $wreq.UserAgent = Get-UserAgent 60 | $wreq.Referer = strip_filename $url 61 | if ($cookies) { 62 | $wreq.Headers.Add('Cookie', (cookie_header $cookies)) 63 | } 64 | } 65 | 66 | get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object { 67 | (ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object { 68 | $wreq.Headers[$_.Key] = $_.Value 69 | } 70 | } 71 | 72 | $wres = $null 73 | try { 74 | $wres = $wreq.GetResponse() 75 | 76 | return $url, $wres.StatusCode, $null 77 | } catch { 78 | $e = $_.Exception 79 | if ($e.InnerException) { $e = $e.InnerException } 80 | 81 | return $url, 'Error', $e.Message 82 | } finally { 83 | if ($null -ne $wres -and $wres -isnot [Net.FtpWebResponse]) { 84 | $wres.Close() 85 | } 86 | } 87 | } 88 | 89 | foreach ($man in $Queue) { 90 | $name, $manifest = $man 91 | $urls = @() 92 | $ok = 0 93 | $failed = 0 94 | $errors = @() 95 | 96 | if ($manifest.url) { 97 | $manifest.url | ForEach-Object { $urls += $_ } 98 | } else { 99 | script:url $manifest '64bit' | ForEach-Object { $urls += $_ } 100 | script:url $manifest '32bit' | ForEach-Object { $urls += $_ } 101 | script:url $manifest 'arm64' | ForEach-Object { $urls += $_ } 102 | } 103 | 104 | $urls | ForEach-Object { 105 | $url, $status, $msg = test_dl $_ $manifest.cookie 106 | if ($msg) { $errors += "$msg ($url)" } 107 | if ($status -eq 'OK' -or $status -eq 'OpeningData') { $ok += 1 } else { $failed += 1 } 108 | } 109 | 110 | if (($ok -eq $urls.Length) -and $SkipValid) { continue } 111 | 112 | # URLS 113 | Write-Host '[' -NoNewLine 114 | Write-Host $urls.Length -NoNewLine -ForegroundColor Cyan 115 | Write-Host ']' -NoNewLine 116 | 117 | # Okay 118 | Write-Host '[' -NoNewLine 119 | if ($ok -eq $urls.Length) { 120 | Write-Host $ok -NoNewLine -ForegroundColor Green 121 | } elseif ($ok -eq 0) { 122 | Write-Host $ok -NoNewLine -ForegroundColor Red 123 | } else { 124 | Write-Host $ok -NoNewLine -ForegroundColor Yellow 125 | } 126 | Write-Host ']' -NoNewLine 127 | 128 | # Failed 129 | Write-Host '[' -NoNewLine 130 | if ($failed -eq 0) { 131 | Write-Host $failed -NoNewLine -ForegroundColor Green 132 | } else { 133 | Write-Host $failed -NoNewLine -ForegroundColor Red 134 | } 135 | Write-Host '] ' -NoNewLine 136 | Write-Host $name 137 | 138 | $errors | ForEach-Object { 139 | Write-Host " > $_" -ForegroundColor DarkRed 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /bin/describe.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Search for application description on homepage. 4 | .PARAMETER App 5 | Manifest name to search. 6 | Placeholders are supported. 7 | .PARAMETER Dir 8 | Where to search for manifest(s). 9 | #> 10 | param( 11 | [String] $App = '*', 12 | [Parameter(Mandatory = $true)] 13 | [ValidateScript( { 14 | if (!(Test-Path $_ -Type Container)) { 15 | throw "$_ is not a directory!" 16 | } else { 17 | $true 18 | } 19 | })] 20 | [String] $Dir 21 | ) 22 | 23 | . "$PSScriptRoot\..\lib\core.ps1" 24 | . "$PSScriptRoot\..\lib\manifest.ps1" 25 | . "$PSScriptRoot\..\lib\description.ps1" 26 | 27 | $Dir = Convert-Path $Dir 28 | $Queue = @() 29 | 30 | Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { 31 | $manifest = parse_json $_.FullName 32 | $Queue += , @($_.BaseName, $manifest) 33 | } 34 | 35 | $Queue | ForEach-Object { 36 | $name, $manifest = $_ 37 | Write-Host "$name`: " -NoNewline 38 | 39 | if (!$manifest.homepage) { 40 | Write-Host "`nNo homepage set." -ForegroundColor Red 41 | return 42 | } 43 | # get description from homepage 44 | try { 45 | $wc = New-Object Net.Webclient 46 | $wc.Headers.Add('User-Agent', (Get-UserAgent)) 47 | $homepage = $wc.DownloadData($manifest.homepage) 48 | $home_html = (Get-Encoding($wc)).GetString($homepage) 49 | } catch { 50 | Write-Host "`n$($_.Exception.Message)" -ForegroundColor Red 51 | return 52 | } 53 | 54 | $description, $descr_method = find_description $manifest.homepage $home_html 55 | if (!$description) { 56 | Write-Host "`nDescription not found ($($manifest.homepage))" -ForegroundColor Red 57 | return 58 | } 59 | 60 | $description = clean_description $description 61 | 62 | Write-Host "(found by $descr_method)" 63 | Write-Host " ""$description""" -ForegroundColor Green 64 | } 65 | -------------------------------------------------------------------------------- /bin/formatjson.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Format manifest. 4 | .PARAMETER App 5 | Manifest to format. 6 | 7 | Wildcards are supported. 8 | .PARAMETER Dir 9 | Where to search for manifest(s). 10 | .EXAMPLE 11 | PS BUCKETROOT> .\bin\formatjson.ps1 12 | Format all manifests inside bucket directory. 13 | .EXAMPLE 14 | PS BUCKETROOT> .\bin\formatjson.ps1 7zip 15 | Format manifest '7zip' inside bucket directory. 16 | #> 17 | param( 18 | [String] $App = '*', 19 | [Parameter(Mandatory = $true)] 20 | [ValidateScript( { 21 | if (!(Test-Path $_ -Type Container)) { 22 | throw "$_ is not a directory!" 23 | } else { 24 | $true 25 | } 26 | })] 27 | [String] $Dir 28 | ) 29 | 30 | . "$PSScriptRoot\..\lib\core.ps1" 31 | . "$PSScriptRoot\..\lib\manifest.ps1" 32 | . "$PSScriptRoot\..\lib\json.ps1" 33 | 34 | $Dir = Convert-Path $Dir 35 | 36 | Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { 37 | $file = $_.FullName 38 | # beautify 39 | $json = parse_json $file | ConvertToPrettyJson 40 | 41 | # convert to 4 spaces 42 | $json = $json -replace "`t", ' ' 43 | [System.IO.File]::WriteAllLines($file, $json) 44 | } 45 | -------------------------------------------------------------------------------- /bin/install.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5 2 | Invoke-RestMethod https://get.scoop.sh | Invoke-Expression 3 | -------------------------------------------------------------------------------- /bin/missing-checkver.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Check if manifest contains checkver and autoupdate property. 4 | .PARAMETER App 5 | Manifest name. 6 | Wirldcard is supported. 7 | .PARAMETER Dir 8 | Location of manifests. 9 | .PARAMETER SkipSupported 10 | Manifests with checkver and autoupdate will not be presented. 11 | #> 12 | param( 13 | [String] $App = '*', 14 | [Parameter(Mandatory = $true)] 15 | [ValidateScript( { 16 | if (!(Test-Path $_ -Type Container)) { 17 | throw "$_ is not a directory!" 18 | } else { 19 | $true 20 | } 21 | })] 22 | [String] $Dir, 23 | [Switch] $SkipSupported 24 | ) 25 | 26 | . "$PSScriptRoot\..\lib\core.ps1" 27 | . "$PSScriptRoot\..\lib\manifest.ps1" 28 | 29 | $Dir = Convert-Path $Dir 30 | 31 | Write-Host '[' -NoNewLine 32 | Write-Host 'C' -NoNewLine -ForegroundColor Green 33 | Write-Host ']heckver' 34 | Write-Host ' | [' -NoNewLine 35 | Write-Host 'A' -NoNewLine -ForegroundColor Cyan 36 | Write-Host ']utoupdate' 37 | Write-Host ' | |' 38 | 39 | Get-ChildItem $Dir -Filter "$App.json" -Recurse | ForEach-Object { 40 | $json = parse_json $_.FullName 41 | 42 | if ($SkipSupported -and $json.checkver -and $json.autoupdate) { return } 43 | 44 | Write-Host '[' -NoNewLine 45 | Write-Host $(if ($json.checkver) { 'C' } else { ' ' }) -NoNewLine -ForegroundColor Green 46 | Write-Host ']' -NoNewLine 47 | 48 | Write-Host '[' -NoNewLine 49 | Write-Host $(if ($json.autoupdate) { 'A' } else { ' ' }) -NoNewLine -ForegroundColor Cyan 50 | Write-Host '] ' -NoNewLine 51 | Write-Host $_.BaseName 52 | } 53 | -------------------------------------------------------------------------------- /bin/refresh.ps1: -------------------------------------------------------------------------------- 1 | # for development, update the installed scripts to match local source 2 | . "$PSScriptRoot\..\lib\core.ps1" 3 | 4 | $src = "$PSScriptRoot\.." 5 | $dest = ensure (versiondir 'scoop' 'current') 6 | 7 | # make sure not running from the installed directory 8 | if("$src" -eq "$dest") { abort "$(strip_ext $myinvocation.mycommand.name) is for development only" } 9 | 10 | 'copying files...' 11 | $output = robocopy $src $dest /mir /njh /njs /nfl /ndl /xd .git tmp /xf .DS_Store last_updated 12 | 13 | $output | Where-Object { $_ -ne "" } 14 | 15 | Write-Output 'creating shim...' 16 | shim "$dest\bin\scoop.ps1" $false 17 | 18 | success 'scoop was refreshed!' 19 | -------------------------------------------------------------------------------- /bin/scoop.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5 2 | Set-StrictMode -Off 3 | 4 | . "$PSScriptRoot\..\lib\core.ps1" 5 | . "$PSScriptRoot\..\lib\buckets.ps1" 6 | . "$PSScriptRoot\..\lib\commands.ps1" 7 | . "$PSScriptRoot\..\lib\help.ps1" 8 | 9 | $subCommand = $Args[0] 10 | 11 | # for aliases where there's a local function, re-alias so the function takes precedence 12 | $aliases = Get-Alias | Where-Object { $_.Options -notmatch 'ReadOnly|AllScope' } | ForEach-Object { $_.Name } 13 | Get-ChildItem Function: | Where-Object -Property Name -In -Value $aliases | ForEach-Object { 14 | Set-Alias -Name $_.Name -Value Local:$($_.Name) -Scope Script 15 | } 16 | 17 | switch ($subCommand) { 18 | ({ $subCommand -in @($null, '-h', '--help', '/?') }) { 19 | exec 'help' 20 | } 21 | ({ $subCommand -in @('-v', '--version') }) { 22 | Write-Host 'Current Scoop version:' 23 | if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') { 24 | Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline') 25 | } else { 26 | $version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md" 27 | Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline 28 | Write-Host " - Released at $($version.Matches.Groups[2].Value)" 29 | } 30 | Write-Host '' 31 | 32 | Get-LocalBucket | ForEach-Object { 33 | $bucketLoc = Find-BucketDirectory $_ -Root 34 | if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) { 35 | Write-Host "'$_' bucket:" 36 | Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline') 37 | Write-Host '' 38 | } 39 | } 40 | } 41 | ({ $subCommand -in (commands) }) { 42 | [string[]]$arguments = $Args | Select-Object -Skip 1 43 | if ($null -ne $arguments -and $arguments[0] -in @('-h', '--help', '/?')) { 44 | exec 'help' @($subCommand) 45 | } else { 46 | exec $subCommand $arguments 47 | } 48 | } 49 | default { 50 | warn "scoop: '$subCommand' isn't a scoop command. See 'scoop help'." 51 | exit 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bin/test.ps1: -------------------------------------------------------------------------------- 1 | . "$PSScriptRoot\..\test\bin\test.ps1" 2 | -------------------------------------------------------------------------------- /bin/uninstall.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uninstall ALL scoop applications and scoop itself. 4 | .PARAMETER global 5 | Global applications will be uninstalled. 6 | .PARAMETER purge 7 | Persisted data will be deleted. 8 | #> 9 | param( 10 | [bool] $global, 11 | [bool] $purge 12 | ) 13 | 14 | . "$PSScriptRoot\..\lib\core.ps1" 15 | . "$PSScriptRoot\..\lib\system.ps1" 16 | . "$PSScriptRoot\..\lib\install.ps1" 17 | . "$PSScriptRoot\..\lib\shortcuts.ps1" 18 | . "$PSScriptRoot\..\lib\versions.ps1" 19 | . "$PSScriptRoot\..\lib\manifest.ps1" 20 | 21 | if ($global -and !(is_admin)) { 22 | error 'You need admin rights to uninstall globally.' 23 | exit 1 24 | } 25 | 26 | if ($purge) { 27 | warn 'This will uninstall Scoop, all the programs that have been installed with Scoop and all persisted data!' 28 | } else { 29 | warn 'This will uninstall Scoop and all the programs that have been installed with Scoop!' 30 | } 31 | $yn = Read-Host 'Are you sure? (yN)' 32 | if ($yn -notlike 'y*') { exit } 33 | 34 | $errors = $false 35 | 36 | # Uninstall given app 37 | function do_uninstall($app, $global) { 38 | $version = Select-CurrentVersion -AppName $app -Global:$global 39 | $dir = versiondir $app $version $global 40 | $manifest = installed_manifest $app $version $global 41 | $install = install_info $app $version $global 42 | $architecture = $install.architecture 43 | 44 | Write-Output "Uninstalling '$app'" 45 | Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall 46 | rm_shims $app $manifest $global $architecture 47 | 48 | # If a junction was used during install, that will have been used 49 | # as the reference directory. Othewise it will just be the version 50 | # directory. 51 | $refdir = unlink_current (appdir $app $global) 52 | 53 | env_rm_path $manifest $refdir $global $architecture 54 | env_rm $manifest $global $architecture 55 | 56 | $appdir = appdir $app $global 57 | try { 58 | Remove-Item $appdir -Recurse -Force -ErrorAction Stop 59 | } catch { 60 | $errors = $true 61 | warn "Couldn't remove $(friendly_path $appdir): $_.Exception" 62 | } 63 | } 64 | 65 | function rm_dir($dir) { 66 | try { 67 | Remove-Item $dir -Recurse -Force -ErrorAction Stop 68 | } catch { 69 | abort "Couldn't remove $(friendly_path $dir): $_" 70 | } 71 | } 72 | 73 | # Remove all folders (except persist) inside given scoop directory. 74 | function keep_onlypersist($directory) { 75 | Get-ChildItem $directory -Exclude 'persist' | ForEach-Object { rm_dir $_ } 76 | } 77 | 78 | # Run uninstallation for each app if necessary, continuing if there's 79 | # a problem deleting a directory (which is quite likely) 80 | if ($global) { 81 | installed_apps $true | ForEach-Object { # global apps 82 | do_uninstall $_ $true 83 | } 84 | } 85 | 86 | installed_apps $false | ForEach-Object { # local apps 87 | do_uninstall $_ $false 88 | } 89 | 90 | if ($errors) { 91 | abort 'Not all apps could be deleted. Try again or restart.' 92 | } 93 | 94 | if ($purge) { 95 | rm_dir $scoopdir 96 | if ($global) { rm_dir $globaldir } 97 | } else { 98 | keep_onlypersist $scoopdir 99 | if ($global) { keep_onlypersist $globaldir } 100 | } 101 | 102 | Remove-Path -Path (shimdir $global) -Global:$global 103 | if (get_config USE_ISOLATED_PATH) { 104 | Remove-Path -Path ('%' + $scoopPathEnvVar + '%') -Global:$global 105 | } 106 | 107 | success 'Scoop has been uninstalled.' 108 | -------------------------------------------------------------------------------- /buckets.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "https://github.com/ScoopInstaller/Main", 3 | "extras": "https://github.com/ScoopInstaller/Extras", 4 | "versions": "https://github.com/ScoopInstaller/Versions", 5 | "nirsoft": "https://github.com/ScoopInstaller/Nirsoft", 6 | "sysinternals": "https://github.com/niheaven/scoop-sysinternals", 7 | "php": "https://github.com/ScoopInstaller/PHP", 8 | "nerd-fonts": "https://github.com/matthewjberger/scoop-nerd-fonts", 9 | "nonportable": "https://github.com/ScoopInstaller/Nonportable", 10 | "java": "https://github.com/ScoopInstaller/Java", 11 | "games": "https://github.com/Calinou/scoop-games" 12 | } 13 | -------------------------------------------------------------------------------- /lib/commands.ps1: -------------------------------------------------------------------------------- 1 | # Description: Functions for managing commands and aliases. 2 | 3 | ## Functions for commands 4 | 5 | function command_files { 6 | (Get-ChildItem "$PSScriptRoot\..\libexec") + (Get-ChildItem "$scoopdir\shims") | 7 | Where-Object 'scoop-.*?\.ps1$' -Property Name -Match 8 | } 9 | 10 | function commands { 11 | command_files | ForEach-Object { command_name $_ } 12 | } 13 | 14 | function command_name($filename) { 15 | $filename.name | Select-String 'scoop-(.*?)\.ps1$' | ForEach-Object { $_.matches[0].groups[1].value } 16 | } 17 | 18 | function command_path($cmd) { 19 | $cmd_path = "$PSScriptRoot\..\libexec\scoop-$cmd.ps1" 20 | 21 | # built in commands 22 | if (!(Test-Path $cmd_path)) { 23 | # get path from shim 24 | $shim_path = "$scoopdir\shims\scoop-$cmd.ps1" 25 | $line = ((Get-Content $shim_path) | Where-Object { $_.startswith('$path') }) 26 | if ($line) { 27 | Invoke-Command ([scriptblock]::Create($line)) -NoNewScope 28 | $cmd_path = $path 29 | } else { $cmd_path = $shim_path } 30 | } 31 | 32 | $cmd_path 33 | } 34 | 35 | function exec($cmd, $arguments) { 36 | $cmd_path = command_path $cmd 37 | 38 | & $cmd_path @arguments 39 | } 40 | 41 | ## Functions for aliases 42 | 43 | function add_alias { 44 | param( 45 | [ValidateNotNullOrEmpty()] 46 | [string]$name, 47 | [ValidateNotNullOrEmpty()] 48 | [string]$command, 49 | [string]$description 50 | ) 51 | 52 | $aliases = get_config ALIAS ([PSCustomObject]@{}) 53 | if ($aliases.$name) { 54 | abort "Alias '$name' already exists." 55 | } 56 | 57 | $alias_script_name = "scoop-$name" 58 | $shimdir = shimdir $false 59 | if (Test-Path "$shimdir\$alias_script_name.ps1") { 60 | abort "File '$alias_script_name.ps1' already exists in shims directory." 61 | } 62 | $script = @( 63 | "# Summary: $description", 64 | "$command" 65 | ) -join "`n" 66 | try { 67 | $script | Out-UTF8File "$shimdir\$alias_script_name.ps1" 68 | } catch { 69 | abort $_.Exception 70 | } 71 | 72 | # Add the new alias to the config. 73 | $aliases | Add-Member -MemberType NoteProperty -Name $name -Value $alias_script_name 74 | set_config ALIAS $aliases | Out-Null 75 | } 76 | 77 | function rm_alias { 78 | param( 79 | [ValidateNotNullOrEmpty()] 80 | [string]$name 81 | ) 82 | 83 | $aliases = get_config ALIAS ([PSCustomObject]@{}) 84 | if (!$aliases.$name) { 85 | abort "Alias '$name' doesn't exist." 86 | } 87 | 88 | info "Removing alias '$name'..." 89 | Remove-Item "$(shimdir $false)\scoop-$name.ps1" 90 | $aliases.PSObject.Properties.Remove($name) 91 | set_config ALIAS $aliases | Out-Null 92 | } 93 | 94 | function list_aliases { 95 | param( 96 | [bool]$verbose 97 | ) 98 | 99 | $aliases = get_config ALIAS ([PSCustomObject]@{}) 100 | $alias_info = $aliases.PSObject.Properties.Name | Where-Object { $_ } | ForEach-Object { 101 | $content = Get-Content (command_path $_) 102 | [PSCustomObject]@{ 103 | Name = $_ 104 | Summary = (summary $content).Trim() 105 | Command = ($content | Select-Object -Skip 1).Trim() 106 | } 107 | } 108 | if (!$alias_info) { 109 | info 'No alias found.' 110 | return 111 | } 112 | $alias_info = $alias_info | Sort-Object Name 113 | $properties = @('Name', 'Command') 114 | if ($verbose) { 115 | $properties += 'Summary' 116 | } 117 | $alias_info | Select-Object $properties 118 | } 119 | -------------------------------------------------------------------------------- /lib/depends.ps1: -------------------------------------------------------------------------------- 1 | function Get-Dependency { 2 | <# 3 | .SYNOPSIS 4 | Get app's dependencies (with apps attached at the end). 5 | .PARAMETER AppName 6 | App's name 7 | .PARAMETER Architecture 8 | App's architecture 9 | .PARAMETER Resolved 10 | List of resolved dependencies (internal use) 11 | .PARAMETER Unresolved 12 | List of unresolved dependencies (internal use) 13 | .OUTPUTS 14 | [Object[]] 15 | List of app's dependencies 16 | .NOTES 17 | When pipeline input is used, the output will have duplicate items, and should be filtered by 'Select-Object -Unique'. 18 | ALgorithm: http://www.electricmonk.nl/docs/dependency_resolving_algorithm/dependency_resolving_algorithm.html 19 | #> 20 | [CmdletBinding()] 21 | [OutputType([Object[]])] 22 | param ( 23 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 24 | [PSObject] 25 | $AppName, 26 | [Parameter(Mandatory = $true, Position = 1)] 27 | [String] 28 | $Architecture, 29 | [String[]] 30 | $Resolved = @(), 31 | [String[]] 32 | $Unresolved = @() 33 | ) 34 | process { 35 | $AppName, $manifest, $bucket, $url = Get-Manifest $AppName 36 | $Unresolved += $AppName 37 | 38 | if (!$manifest) { 39 | if (((Get-LocalBucket) -notcontains $bucket) -and $bucket) { 40 | warn "Bucket '$bucket' not added. Add it with $(if($bucket -in (known_buckets)) { "'scoop bucket add $bucket' or " })'scoop bucket add $bucket '." 41 | } 42 | abort "Couldn't find manifest for '$AppName'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." 43 | } 44 | 45 | $deps = @(Get-InstallationHelper $manifest $Architecture) + @($manifest.depends) | Select-Object -Unique 46 | 47 | foreach ($dep in $deps) { 48 | if ($Resolved -notcontains $dep) { 49 | if ($Unresolved -contains $dep) { 50 | abort "Circular dependency detected: '$AppName' -> '$dep'." 51 | } 52 | $Resolved, $Unresolved = Get-Dependency $dep $Architecture -Resolved $Resolved -Unresolved $Unresolved 53 | } 54 | } 55 | 56 | $Unresolved = $Unresolved -ne $AppName 57 | if ($bucket) { 58 | $Resolved += "$bucket/$AppName" 59 | } else { 60 | if ($url) { 61 | $Resolved += $url 62 | } else { 63 | $Resolved += $AppName 64 | } 65 | } 66 | if ($Unresolved.Length -eq 0) { 67 | return $Resolved 68 | } else { 69 | return $Resolved, $Unresolved 70 | } 71 | } 72 | } 73 | 74 | function Get-InstallationHelper { 75 | <# 76 | .SYNOPSIS 77 | Get helpers that used in installation 78 | .PARAMETER Manifest 79 | App's manifest 80 | .PARAMETER Architecture 81 | Architecture of the app 82 | .PARAMETER All 83 | If true, return all helpers, otherwise return only helpers that are not already installed 84 | .OUTPUTS 85 | [Object[]] 86 | List of helpers 87 | #> 88 | [CmdletBinding()] 89 | [OutputType([Object[]])] 90 | param ( 91 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 92 | [PSObject] 93 | $Manifest, 94 | [Parameter(Mandatory = $true, Position = 1)] 95 | [String] 96 | $Architecture, 97 | [Switch] 98 | $All 99 | ) 100 | begin { 101 | $helper = @() 102 | } 103 | process { 104 | $url = arch_specific 'url' $Manifest $Architecture 105 | $pre_install = arch_specific 'pre_install' $Manifest $Architecture 106 | $installer = arch_specific 'installer' $Manifest $Architecture 107 | $post_install = arch_specific 'post_install' $Manifest $Architecture 108 | $script = $pre_install + $installer.script + $post_install 109 | if (((Test-7zipRequirement -Uri $url) -or ($script -like '*Expand-7zipArchive *')) -and !(get_config USE_EXTERNAL_7ZIP)) { 110 | $helper += '7zip' 111 | } 112 | if (((Test-LessmsiRequirement -Uri $url) -or ($script -like '*Expand-MsiArchive *')) -and (get_config USE_LESSMSI)) { 113 | $helper += 'lessmsi' 114 | } 115 | if ($Manifest.innosetup -or ($script -like '*Expand-InnoArchive *')) { 116 | $helper += 'innounp' 117 | } 118 | if ($script -like '*Expand-DarkArchive *') { 119 | $helper += 'dark' 120 | } 121 | if (!$All) { 122 | '7zip', 'lessmsi', 'innounp', 'dark' | ForEach-Object { 123 | if (Test-HelperInstalled -Helper $_) { 124 | $helper = $helper -ne $_ 125 | } 126 | } 127 | } 128 | } 129 | end { 130 | return $helper 131 | } 132 | } 133 | 134 | function Test-7zipRequirement { 135 | [CmdletBinding()] 136 | [OutputType([Boolean])] 137 | param ( 138 | [Parameter(Mandatory = $true)] 139 | [AllowNull()] 140 | [String[]] 141 | $Uri 142 | ) 143 | return ($Uri | Where-Object { 144 | $_ -match '\.(001|7z|bz(ip)?2?|gz|img|iso|lzma|lzh|nupkg|rar|tar|t[abgpx]z2?|t?zst|xz)(\.[^\d.]+)?$' 145 | }).Count -gt 0 146 | } 147 | 148 | function Test-LessmsiRequirement { 149 | [CmdletBinding()] 150 | [OutputType([Boolean])] 151 | param ( 152 | [Parameter(Mandatory = $true)] 153 | [AllowNull()] 154 | [String[]] 155 | $Uri 156 | ) 157 | return ($Uri | Where-Object { $_ -match '\.msi$' }).Count -gt 0 158 | } 159 | -------------------------------------------------------------------------------- /lib/description.ps1: -------------------------------------------------------------------------------- 1 | function find_description($url, $html, $redir = $false) { 2 | $meta = meta_tags $html 3 | 4 | # check 5 | $og_description = meta_content $meta 'property' 'og:description' 6 | if($og_description) { 7 | return $og_description, '' 8 | } 9 | 10 | # check 11 | $description = meta_content $meta 'name' 'description' 12 | if($description) { 13 | return $description, '' 14 | } 15 | 16 | # check redirect 17 | $refresh = meta_refresh $meta $url 18 | if($refresh -and !$redir) { 19 | $wc = New-Object Net.Webclient 20 | $wc.Headers.Add('User-Agent', (Get-UserAgent)) 21 | $data = $wc.DownloadData($refresh) 22 | $html = (Get-Encoding($wc)).GetString($data) 23 | return find_description $refresh $html $true 24 | } 25 | 26 | # check text for 'x is ...' 27 | $text = html_text $html $meta 28 | $text_desc = find_is $text 29 | if($text_desc) { 30 | return $text_desc, 'text' 31 | } 32 | 33 | # first paragraph 34 | $first_para = first_para $html 35 | if($first_para) { 36 | return $first_para, 'first

' 37 | } 38 | 39 | return $null, $null 40 | } 41 | 42 | function clean_description($description) { 43 | if(!$description) { return $description } 44 | $description = $description -replace '\n', ' ' 45 | $description = $description -replace '\s{2,}', ' ' 46 | return $description.trim() 47 | } 48 | 49 | # Collects meta tags from $html into hashtables. 50 | function meta_tags($html) { 51 | $tags = @() 52 | $meta = ([regex]']+>').matches($html) 53 | $meta | ForEach-Object { 54 | $attrs = ([regex]'([\w-]+)="([^"]+)"').matches($_.value) 55 | $hash = @{} 56 | $attrs | ForEach-Object { 57 | $hash[$_.groups[1].value] = $_.groups[2].value 58 | } 59 | $tags += $hash 60 | } 61 | $tags 62 | } 63 | 64 | function meta_content($tags, $attribute, $search) { 65 | if(!$tags) { return } 66 | return $tags | Where-Object { $_[$attribute] -eq $search } | ForEach-Object { $_['content'] } 67 | } 68 | 69 | # Looks for a redirect URL in a refresh tag. 70 | function meta_refresh($tags, $url) { 71 | $refresh = meta_content $tags 'http-equiv' 'refresh' 72 | if($refresh) { 73 | if($refresh -match '\d+;\s*url\s*=\s*(.*)') { 74 | $refresh_url = $matches[1].trim("'", '"') 75 | if($refresh_url -notmatch '^https?://') { 76 | $refresh_url = "$url$refresh_url" 77 | } 78 | return $refresh_url 79 | } 80 | } 81 | } 82 | 83 | function html_body($html) { 84 | if($html -match '(?s)]*>(.*?)') { 85 | $body = $matches[1] 86 | $body = $body -replace '(?s)]*>.*?', ' ' 87 | $body = $body -replace '(?s)', ' ' 88 | return $body 89 | } 90 | } 91 | 92 | function html_text($body, $meta_tags) { 93 | $body = html_body $html 94 | if($body) { 95 | return strip_html $body 96 | } 97 | } 98 | 99 | function strip_html($html) { 100 | $html = $html -replace '(?s)<[^>]*>', ' ' 101 | $html = $html -replace '\t', ' ' 102 | $html = $html -replace ' ?', ' ' 103 | $html = $html -replace '>?', '>' 104 | $html = $html -replace '<?', '<' 105 | $html = $html -replace '"?', '"' 106 | 107 | $encoding_meta = meta_content $meta_tags 'http-equiv' 'Content-Type' 108 | if($encoding_meta) { 109 | if($encoding_meta -match 'charset\s*=\s*(.*)') { 110 | $charset = $matches[1] 111 | try { 112 | $encoding = [text.encoding]::getencoding($charset) 113 | } catch { 114 | Write-Warning "Unknown charset" 115 | } 116 | if($encoding) { 117 | $html = ([regex]'&#(\d+);?').replace($html, { 118 | param($m) 119 | try { 120 | return $encoding.getstring($m.Groups[1].Value) 121 | } catch { 122 | return $m.value 123 | } 124 | }) 125 | } 126 | } 127 | } 128 | 129 | $html = $html -replace '\n +', "`r`n" 130 | $html = $html -replace '\n{2,}', "`r`n" 131 | $html = $html -replace ' {2,}', ' ' 132 | $html = $html -replace ' (\.|,)', '$1' 133 | return $html.trim() 134 | } 135 | 136 | function find_is($text) { 137 | if($text -match '(?s)[\n\.]((?:[^\n\.])+? is .+?[\.!])') { 138 | return $matches[1].trim() 139 | } 140 | } 141 | 142 | function first_para($html) { 143 | $body = html_body $html 144 | if($body -match '(?s)]*>(.*?)

') { 145 | return strip_html $matches[1] 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/diagnostic.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Diagnostic tests. 3 | Return $true if the test passed, otherwise $false. 4 | Use 'warn' to highlight the issue, and follow up with the recommended actions to rectify. 5 | #> 6 | function check_windows_defender($global) { 7 | $defender = Get-Service -Name WinDefend -ErrorAction SilentlyContinue 8 | if (Test-CommandAvailable Get-MpPreference) { 9 | if ((Get-MpPreference).DisableRealtimeMonitoring) { return $true } 10 | if ($defender -and $defender.Status) { 11 | if ($defender.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Running) { 12 | $installPath = $scoopdir; 13 | if ($global) { $installPath = $globaldir; } 14 | 15 | $exclusionPath = (Get-MpPreference).ExclusionPath 16 | if (!($exclusionPath -contains $installPath)) { 17 | info "Windows Defender may slow down or disrupt installs with realtime scanning." 18 | Write-Host " Consider running:" 19 | Write-Host " sudo Add-MpPreference -ExclusionPath '$installPath'" 20 | Write-Host " (Requires 'sudo' command. Run 'scoop install sudo' if you don't have it.)" 21 | return $false 22 | } 23 | } 24 | } 25 | } 26 | return $true 27 | } 28 | 29 | function check_main_bucket { 30 | if ((Get-LocalBucket) -notcontains 'main') { 31 | warn 'Main bucket is not added.' 32 | Write-Host " run 'scoop bucket add main'" 33 | 34 | return $false 35 | } 36 | 37 | return $true 38 | } 39 | 40 | function check_long_paths { 41 | if ([System.Environment]::OSVersion.Version.Major -lt 10 -or [System.Environment]::OSVersion.Version.Build -lt 1607) { 42 | warn 'This version of Windows does not support configuration of LongPaths.' 43 | return $false 44 | } 45 | $key = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -ErrorAction SilentlyContinue -Name 'LongPathsEnabled' 46 | if (!$key -or ($key.LongPathsEnabled -eq 0)) { 47 | warn 'LongPaths support is not enabled.' 48 | Write-Host " You can enable it by running:" 49 | Write-Host " sudo Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1" 50 | Write-Host " (Requires 'sudo' command. Run 'scoop install sudo' if you don't have it.)" 51 | return $false 52 | } 53 | 54 | return $true 55 | } 56 | 57 | function Get-WindowsDeveloperModeStatus { 58 | $DevModRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" 59 | if (!(Test-Path -Path $DevModRegistryPath) -or (Get-ItemProperty -Path ` 60 | $DevModRegistryPath -Name AllowDevelopmentWithoutDevLicense -ErrorAction ` 61 | SilentlyContinue).AllowDevelopmentWithoutDevLicense -ne 1) { 62 | warn "Windows Developer Mode is not enabled. Operations relevant to symlinks may fail without proper rights." 63 | Write-Host " You may read more about the symlinks support here:" 64 | Write-Host " https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/" 65 | return $false 66 | } 67 | 68 | return $true 69 | } 70 | -------------------------------------------------------------------------------- /lib/getopt.ps1: -------------------------------------------------------------------------------- 1 | # adapted from http://hg.python.org/cpython/file/2.7/Lib/getopt.py 2 | # argv: 3 | # array of arguments 4 | # shortopts: 5 | # string of single-letter options. options that take a parameter 6 | # should be follow by ':' 7 | # longopts: 8 | # array of strings that are long-form options. options that take 9 | # a parameter should end with '=' 10 | # returns @(opts hash, remaining_args array, error string) 11 | # NOTES: 12 | # The first "--" in $argv, if any, will terminate all options; any 13 | # following arguments are treated as non-option arguments, even if 14 | # they begin with a hyphen. The "--" itself will not be included in 15 | # the returned $opts. (POSIX-compatible) 16 | function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) { 17 | $opts = @{}; $rem = @() 18 | 19 | function err($msg) { 20 | $opts, $rem, $msg 21 | } 22 | 23 | function regex_escape($str) { 24 | return [Regex]::Escape($str) 25 | } 26 | 27 | for ($i = 0; $i -lt $argv.Length; $i++) { 28 | $arg = $argv[$i] 29 | if ($null -eq $arg) { continue } 30 | # don't try to parse array arguments 31 | if ($arg -is [Array]) { $rem += , $arg; continue } 32 | if ($arg -is [Int]) { $rem += $arg; continue } 33 | if ($arg -is [Decimal]) { $rem += $arg; continue } 34 | 35 | if ($arg -eq '--') { 36 | if ($i -lt $argv.Length - 1) { 37 | $rem += $argv[($i + 1)..($argv.Length - 1)] 38 | } 39 | break 40 | } elseif ($arg.StartsWith('--')) { 41 | $name = $arg.Substring(2) 42 | 43 | $longopt = $longopts | Where-Object { $_ -match "^$name=?$" } 44 | 45 | if ($longopt) { 46 | if ($longopt.EndsWith('=')) { 47 | # requires arg 48 | if ($i -eq $argv.Length - 1) { 49 | return err "Option --$name requires an argument." 50 | } 51 | $opts.$name = $argv[++$i] 52 | } else { 53 | $opts.$name = $true 54 | } 55 | } else { 56 | return err "Option --$name not recognized." 57 | } 58 | } elseif ($arg.StartsWith('-') -and $arg -ne '-') { 59 | for ($j = 1; $j -lt $arg.Length; $j++) { 60 | $letter = $arg[$j].ToString() 61 | 62 | if ($shortopts -match "$(regex_escape $letter)`:?") { 63 | $shortopt = $Matches[0] 64 | if ($shortopt[1] -eq ':') { 65 | if ($j -ne $arg.Length - 1 -or $i -eq $argv.Length - 1) { 66 | return err "Option -$letter requires an argument." 67 | } 68 | $opts.$letter = $argv[++$i] 69 | } else { 70 | $opts.$letter = $true 71 | } 72 | } else { 73 | return err "Option -$letter not recognized." 74 | } 75 | } 76 | } else { 77 | $rem += $arg 78 | } 79 | } 80 | $opts, $rem 81 | } 82 | -------------------------------------------------------------------------------- /lib/help.ps1: -------------------------------------------------------------------------------- 1 | function usage($text) { 2 | $text | Select-String '(?m)^# Usage: ([^\n]*)$' | ForEach-Object { "Usage: " + $_.matches[0].groups[1].value } 3 | } 4 | 5 | function summary($text) { 6 | $text | Select-String '(?m)^# Summary: ([^\n]*)$' | ForEach-Object { $_.matches[0].groups[1].value } 7 | } 8 | 9 | function scoop_help($text) { 10 | $help_lines = $text | Select-String '(?ms)^# Help:(.(?!^[^#]))*' | ForEach-Object { $_.matches[0].value; } 11 | $help_lines -replace '(?ms)^#\s?(Help: )?', '' 12 | } 13 | 14 | function my_usage { # gets usage for the calling script 15 | usage (Get-Content $myInvocation.PSCommandPath -raw) 16 | } 17 | -------------------------------------------------------------------------------- /lib/manifest.ps1: -------------------------------------------------------------------------------- 1 | function manifest_path($app, $bucket) { 2 | (Get-ChildItem (Find-BucketDirectory $bucket) -Filter "$(sanitary_path $app).json" -Recurse).FullName 3 | } 4 | 5 | function parse_json($path) { 6 | if ($null -eq $path -or !(Test-Path $path)) { return $null } 7 | try { 8 | Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop 9 | } catch { 10 | warn "Error parsing JSON at '$path'." 11 | } 12 | } 13 | 14 | function url_manifest($url) { 15 | $str = $null 16 | try { 17 | $wc = New-Object Net.Webclient 18 | $wc.Headers.Add('User-Agent', (Get-UserAgent)) 19 | $data = $wc.DownloadData($url) 20 | $str = (Get-Encoding($wc)).GetString($data) 21 | } catch [system.management.automation.methodinvocationexception] { 22 | warn "error: $($_.exception.innerexception.message)" 23 | } catch { 24 | throw 25 | } 26 | if (!$str) { return $null } 27 | try { 28 | $str | ConvertFrom-Json -ErrorAction Stop 29 | } catch { 30 | warn "Error parsing JSON at '$url'." 31 | } 32 | } 33 | 34 | function Get-Manifest($app) { 35 | $bucket, $manifest, $url = $null 36 | $app = $app.TrimStart('/') 37 | # check if app is a URL or UNC path 38 | if ($app -match '^(ht|f)tps?://|\\\\') { 39 | $url = $app 40 | $app = appname_from_url $url 41 | $manifest = url_manifest $url 42 | } else { 43 | $app, $bucket, $version = parse_app $app 44 | if ($bucket) { 45 | $manifest = manifest $app $bucket 46 | } else { 47 | foreach ($tekcub in Get-LocalBucket) { 48 | $manifest = manifest $app $tekcub 49 | if ($manifest) { 50 | $bucket = $tekcub 51 | break 52 | } 53 | } 54 | } 55 | if (!$manifest) { 56 | # couldn't find app in buckets: check if it's a local path 57 | if (Test-Path $app) { 58 | $url = Convert-Path $app 59 | $app = appname_from_url $url 60 | $manifest = url_manifest $url 61 | } else { 62 | if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app } 63 | $app = appname_from_url $app 64 | } 65 | } 66 | } 67 | return $app, $manifest, $bucket, $url 68 | } 69 | 70 | function manifest($app, $bucket, $url) { 71 | if ($url) { return url_manifest $url } 72 | parse_json (manifest_path $app $bucket) 73 | } 74 | 75 | function save_installed_manifest($app, $bucket, $dir, $url) { 76 | if ($url) { 77 | $wc = New-Object Net.Webclient 78 | $wc.Headers.Add('User-Agent', (Get-UserAgent)) 79 | $data = $wc.DownloadData($url) 80 | (Get-Encoding($wc)).GetString($data) | Out-UTF8File "$dir\manifest.json" 81 | } else { 82 | Copy-Item (manifest_path $app $bucket) "$dir\manifest.json" 83 | } 84 | } 85 | 86 | function installed_manifest($app, $version, $global) { 87 | parse_json "$(versiondir $app $version $global)\manifest.json" 88 | } 89 | 90 | function save_install_info($info, $dir) { 91 | $nulls = $info.keys | Where-Object { $null -eq $info[$_] } 92 | $nulls | ForEach-Object { $info.remove($_) } # strip null-valued 93 | 94 | $file_content = $info | ConvertToPrettyJson # in 'json.ps1' 95 | [System.IO.File]::WriteAllLines("$dir\install.json", $file_content) 96 | } 97 | 98 | function install_info($app, $version, $global) { 99 | $path = "$(versiondir $app $version $global)\install.json" 100 | if (!(Test-Path $path)) { return $null } 101 | parse_json $path 102 | } 103 | 104 | function arch_specific($prop, $manifest, $architecture) { 105 | if ($manifest.architecture) { 106 | $val = $manifest.architecture.$architecture.$prop 107 | if ($val) { return $val } # else fallback to generic prop 108 | } 109 | 110 | if ($manifest.$prop) { return $manifest.$prop } 111 | } 112 | 113 | function Get-SupportedArchitecture($manifest, $architecture) { 114 | if ($architecture -eq 'arm64' -and ($manifest | ConvertToPrettyJson) -notmatch '[''"]arm64["'']') { 115 | # Windows 10 enables existing unmodified x86 apps to run on Arm devices. 116 | # Windows 11 adds the ability to run unmodified x64 Windows apps on Arm devices! 117 | # Ref: https://learn.microsoft.com/en-us/windows/arm/overview 118 | if ($WindowsBuild -ge 22000) { 119 | # Windows 11 120 | $architecture = '64bit' 121 | } else { 122 | # Windows 10 123 | $architecture = '32bit' 124 | } 125 | } 126 | if (![String]::IsNullOrEmpty((arch_specific 'url' $manifest $architecture))) { 127 | return $architecture 128 | } 129 | } 130 | 131 | function generate_user_manifest($app, $bucket, $version) { 132 | # 'autoupdate.ps1' 'buckets.ps1' 'manifest.ps1' 133 | $app, $manifest, $bucket, $null = Get-Manifest "$bucket/$app" 134 | if ("$($manifest.version)" -eq "$version") { 135 | return manifest_path $app $bucket 136 | } 137 | warn "Given version ($version) does not match manifest ($($manifest.version))" 138 | warn "Attempting to generate manifest for '$app' ($version)" 139 | 140 | ensure (usermanifestsdir) | Out-Null 141 | $manifest_path = "$(usermanifestsdir)\$app.json" 142 | 143 | if (get_config USE_SQLITE_CACHE) { 144 | $cached_manifest = (Get-ScoopDBItem -Name $app -Bucket $bucket -Version $version).manifest 145 | if ($cached_manifest) { 146 | $cached_manifest | Out-UTF8File $manifest_path 147 | return $manifest_path 148 | } 149 | } 150 | 151 | if (!($manifest.autoupdate)) { 152 | abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'" 153 | } 154 | 155 | try { 156 | Invoke-AutoUpdate $app $manifest_path $manifest $version $(@{ }) 157 | return $manifest_path 158 | } catch { 159 | Write-Host -ForegroundColor DarkRed "Could not install $app@$version" 160 | } 161 | 162 | return $null 163 | } 164 | 165 | function url($manifest, $arch) { arch_specific 'url' $manifest $arch } 166 | function installer($manifest, $arch) { arch_specific 'installer' $manifest $arch } 167 | function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $arch } 168 | function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch } 169 | function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch } 170 | function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch } 171 | -------------------------------------------------------------------------------- /lib/psmodules.ps1: -------------------------------------------------------------------------------- 1 | function install_psmodule($manifest, $dir, $global) { 2 | $psmodule = $manifest.psmodule 3 | if (!$psmodule) { return } 4 | 5 | $targetdir = ensure (modulesdir $global) 6 | 7 | ensure_in_psmodulepath $targetdir $global 8 | 9 | $module_name = $psmodule.name 10 | if (!$module_name) { 11 | abort "Invalid manifest: The 'name' property is missing from 'psmodule'." 12 | } 13 | 14 | $linkfrom = "$targetdir\$module_name" 15 | Write-Host "Installing PowerShell module '$module_name'" 16 | 17 | Write-Host "Linking $(friendly_path $linkfrom) => $(friendly_path $dir)" 18 | 19 | if (Test-Path $linkfrom) { 20 | warn "$(friendly_path $linkfrom) already exists. It will be replaced." 21 | Remove-Item -Path $linkfrom -Force -Recurse -ErrorAction SilentlyContinue 22 | } 23 | 24 | New-DirectoryJunction $linkfrom $dir | Out-Null 25 | } 26 | 27 | function uninstall_psmodule($manifest, $dir, $global) { 28 | $psmodule = $manifest.psmodule 29 | if (!$psmodule) { return } 30 | 31 | $module_name = $psmodule.name 32 | Write-Host "Uninstalling PowerShell module '$module_name'." 33 | 34 | $targetdir = modulesdir $global 35 | 36 | $linkfrom = "$targetdir\$module_name" 37 | if (Test-Path $linkfrom) { 38 | Write-Host "Removing $(friendly_path $linkfrom)" 39 | $linkfrom = Convert-Path $linkfrom 40 | Remove-Item -Path $linkfrom -Force -Recurse -ErrorAction SilentlyContinue 41 | } 42 | } 43 | 44 | function ensure_in_psmodulepath($dir, $global) { 45 | $path = Get-EnvVar -Name 'PSModulePath' -Global:$global 46 | if (!$global -and $null -eq $path) { 47 | $path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" 48 | } 49 | if ($path -notmatch [Regex]::Escape($dir)) { 50 | Write-Output "Adding $(friendly_path $dir) to $(if($global){'global'}else{'your'}) PowerShell module path." 51 | 52 | Set-EnvVar -Name 'PSModulePath' -Value "$dir;$path" -Global:$global 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/shortcuts.ps1: -------------------------------------------------------------------------------- 1 | # Creates shortcut for the app in the start menu 2 | function create_startmenu_shortcuts($manifest, $dir, $global, $arch) { 3 | $shortcuts = @(arch_specific 'shortcuts' $manifest $arch) 4 | $shortcuts | Where-Object { $_ -ne $null } | ForEach-Object { 5 | $target = [System.IO.Path]::Combine($dir, $_.item(0)) 6 | $target = New-Object System.IO.FileInfo($target) 7 | $name = $_.item(1) 8 | $arguments = '' 9 | $icon = $null 10 | if ($_.length -ge 3) { 11 | $arguments = $_.item(2) 12 | } 13 | if ($_.length -ge 4) { 14 | $icon = [System.IO.Path]::Combine($dir, $_.item(3)) 15 | $icon = New-Object System.IO.FileInfo($icon) 16 | } 17 | $arguments = (substitute $arguments @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }) 18 | startmenu_shortcut $target $name $arguments $icon $global 19 | } 20 | } 21 | 22 | function shortcut_folder($global) { 23 | if ($global) { 24 | $startmenu = 'CommonStartMenu' 25 | } else { 26 | $startmenu = 'StartMenu' 27 | } 28 | return Convert-Path (ensure ([System.IO.Path]::Combine([Environment]::GetFolderPath($startmenu), 'Programs', 'Scoop Apps'))) 29 | } 30 | 31 | function startmenu_shortcut([System.IO.FileInfo] $target, $shortcutName, $arguments, [System.IO.FileInfo]$icon, $global) { 32 | if (!$target.Exists) { 33 | Write-Host -f DarkRed "Creating shortcut for $shortcutName ($(fname $target)) failed: Couldn't find $target" 34 | return 35 | } 36 | if ($icon -and !$icon.Exists) { 37 | Write-Host -f DarkRed "Creating shortcut for $shortcutName ($(fname $target)) failed: Couldn't find icon $icon" 38 | return 39 | } 40 | 41 | $scoop_startmenu_folder = shortcut_folder $global 42 | $subdirectory = [System.IO.Path]::GetDirectoryName($shortcutName) 43 | if ($subdirectory) { 44 | $subdirectory = ensure $([System.IO.Path]::Combine($scoop_startmenu_folder, $subdirectory)) 45 | } 46 | 47 | $wsShell = New-Object -ComObject WScript.Shell 48 | $wsShell = $wsShell.CreateShortcut("$scoop_startmenu_folder\$shortcutName.lnk") 49 | $wsShell.TargetPath = $target.FullName 50 | $wsShell.WorkingDirectory = $target.DirectoryName 51 | if ($arguments) { 52 | $wsShell.Arguments = $arguments 53 | } 54 | if ($icon -and $icon.Exists) { 55 | $wsShell.IconLocation = $icon.FullName 56 | } 57 | $wsShell.Save() 58 | Write-Host "Creating shortcut for $shortcutName ($(fname $target))" 59 | } 60 | 61 | # Removes the Startmenu shortcut if it exists 62 | function rm_startmenu_shortcuts($manifest, $global, $arch) { 63 | $shortcuts = @(arch_specific 'shortcuts' $manifest $arch) 64 | $shortcuts | Where-Object { $_ -ne $null } | ForEach-Object { 65 | $name = $_.item(1) 66 | $shortcut = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$(shortcut_folder $global)\$name.lnk") 67 | Write-Host "Removing shortcut $(friendly_path $shortcut)" 68 | if (Test-Path -Path $shortcut) { 69 | Remove-Item $shortcut 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/system.ps1: -------------------------------------------------------------------------------- 1 | # System-related functions 2 | 3 | ## Environment Variables 4 | 5 | function Publish-EnvVar { 6 | if (-not ('Win32.NativeMethods' -as [Type])) { 7 | Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @' 8 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] 9 | public static extern IntPtr SendMessageTimeout( 10 | IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, 11 | uint fuFlags, uint uTimeout, out UIntPtr lpdwResult 12 | ); 13 | '@ 14 | } 15 | 16 | $HWND_BROADCAST = [IntPtr] 0xffff 17 | $WM_SETTINGCHANGE = 0x1a 18 | $result = [UIntPtr]::Zero 19 | 20 | [Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, 21 | $WM_SETTINGCHANGE, 22 | [UIntPtr]::Zero, 23 | 'Environment', 24 | 2, 25 | 5000, 26 | [ref] $result 27 | ) | Out-Null 28 | } 29 | 30 | function Get-EnvVar { 31 | param( 32 | [string]$Name, 33 | [switch]$Global 34 | ) 35 | 36 | $registerKey = if ($Global) { 37 | Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' 38 | } else { 39 | Get-Item -Path 'HKCU:' 40 | } 41 | $envRegisterKey = $registerKey.OpenSubKey('Environment') 42 | $registryValueOption = [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames 43 | $envRegisterKey.GetValue($Name, $null, $registryValueOption) 44 | } 45 | 46 | function Set-EnvVar { 47 | param( 48 | [string]$Name, 49 | [string]$Value, 50 | [switch]$Global 51 | ) 52 | 53 | $registerKey = if ($Global) { 54 | Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' 55 | } else { 56 | Get-Item -Path 'HKCU:' 57 | } 58 | $envRegisterKey = $registerKey.OpenSubKey('Environment', $true) 59 | if ($null -eq $Value -or $Value -eq '') { 60 | if ($envRegisterKey.GetValue($Name)) { 61 | $envRegisterKey.DeleteValue($Name) 62 | } 63 | } else { 64 | $registryValueKind = if ($Value.Contains('%')) { 65 | [Microsoft.Win32.RegistryValueKind]::ExpandString 66 | } elseif ($envRegisterKey.GetValue($Name)) { 67 | $envRegisterKey.GetValueKind($Name) 68 | } else { 69 | [Microsoft.Win32.RegistryValueKind]::String 70 | } 71 | $envRegisterKey.SetValue($Name, $Value, $registryValueKind) 72 | } 73 | Publish-EnvVar 74 | } 75 | 76 | function Split-PathLikeEnvVar { 77 | param( 78 | [string[]]$Pattern, 79 | [string]$Path 80 | ) 81 | 82 | if ($null -eq $Path -and $Path -eq '') { 83 | return $null, $null 84 | } else { 85 | $splitPattern = $Pattern.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) 86 | $splitPath = $Path.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) 87 | $inPath = @() 88 | foreach ($p in $splitPattern) { 89 | $inPath += $splitPath.Where({ $_ -like $p }) 90 | $splitPath = $splitPath.Where({ $_ -notlike $p }) 91 | } 92 | return ($inPath -join ';'), ($splitPath -join ';') 93 | } 94 | } 95 | 96 | function Add-Path { 97 | param( 98 | [string[]]$Path, 99 | [string]$TargetEnvVar = 'PATH', 100 | [switch]$Global, 101 | [switch]$Force, 102 | [switch]$Quiet 103 | ) 104 | 105 | # future sessions 106 | $inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global) 107 | if (!$inPath -or $Force) { 108 | if (!$Quiet) { 109 | $Path | ForEach-Object { 110 | Write-Host "Adding $(friendly_path $_) to $(if ($Global) {'global'} else {'your'}) path." 111 | } 112 | } 113 | Set-EnvVar -Name $TargetEnvVar -Value ((@($Path) + $strippedPath) -join ';') -Global:$Global 114 | } 115 | # current session 116 | $inPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH 117 | if (!$inPath -or $Force) { 118 | $env:PATH = (@($Path) + $strippedPath) -join ';' 119 | } 120 | } 121 | 122 | function Remove-Path { 123 | param( 124 | [string[]]$Path, 125 | [string]$TargetEnvVar = 'PATH', 126 | [switch]$Global, 127 | [switch]$Quiet, 128 | [switch]$PassThru 129 | ) 130 | 131 | # future sessions 132 | $inPath, $strippedPath = Split-PathLikeEnvVar $Path (Get-EnvVar -Name $TargetEnvVar -Global:$Global) 133 | if ($inPath) { 134 | if (!$Quiet) { 135 | $Path | ForEach-Object { 136 | Write-Host "Removing $(friendly_path $_) from $(if ($Global) {'global'} else {'your'}) path." 137 | } 138 | } 139 | Set-EnvVar -Name $TargetEnvVar -Value $strippedPath -Global:$Global 140 | } 141 | # current session 142 | $inSessionPath, $strippedPath = Split-PathLikeEnvVar $Path $env:PATH 143 | if ($inSessionPath) { 144 | $env:PATH = $strippedPath 145 | } 146 | if ($PassThru) { 147 | return $inPath 148 | } 149 | } 150 | 151 | ## Deprecated functions 152 | 153 | function env($name, $global, $val) { 154 | if ($PSBoundParameters.ContainsKey('val')) { 155 | Show-DeprecatedWarning $MyInvocation 'Set-EnvVar' 156 | Set-EnvVar -Name $name -Value $val -Global:$global 157 | } else { 158 | Show-DeprecatedWarning $MyInvocation 'Get-EnvVar' 159 | Get-EnvVar -Name $name -Global:$global 160 | } 161 | } 162 | 163 | function strip_path($orig_path, $dir) { 164 | Show-DeprecatedWarning $MyInvocation 'Split-PathLikeEnvVar' 165 | Split-PathLikeEnvVar -Pattern @($dir) -Path $orig_path 166 | } 167 | 168 | function add_first_in_path($dir, $global) { 169 | Show-DeprecatedWarning $MyInvocation 'Add-Path' 170 | Add-Path -Path $dir -Global:$global -Force 171 | } 172 | 173 | function remove_from_path($dir, $global) { 174 | Show-DeprecatedWarning $MyInvocation 'Remove-Path' 175 | Remove-Path -Path $dir -Global:$global 176 | } 177 | -------------------------------------------------------------------------------- /libexec/scoop-alias.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop alias [options] [] 2 | # Summary: Manage scoop aliases 3 | # Help: Available subcommands: add, rm, list. 4 | # 5 | # Aliases are custom Scoop subcommands that can be created to make common tasks easier. 6 | # 7 | # To add an alias: 8 | # 9 | # scoop alias add [] 10 | # 11 | # e.g., 12 | # 13 | # scoop alias add rm 'scoop uninstall $args[0]' 'Uninstall an app' 14 | # scoop alias add upgrade 'scoop update *' 'Update all apps, just like "brew" or "apt"' 15 | # 16 | # To remove an alias: 17 | # 18 | # scoop alias rm 19 | # 20 | # To list all aliases: 21 | # 22 | # scoop alias list [-v|--verbose] 23 | # 24 | # Options: 25 | # -v, --verbose Show alias description and table headers (works only for "list") 26 | 27 | param($SubCommand) 28 | 29 | . "$PSScriptRoot\..\lib\getopt.ps1" 30 | 31 | $SubCommands = @('add', 'rm', 'list') 32 | if ($SubCommand -notin $SubCommands) { 33 | if (!$SubCommand) { 34 | error ' missing' 35 | } else { 36 | error "'$SubCommand' is not one of available subcommands: $($SubCommands -join ', ')" 37 | } 38 | my_usage 39 | exit 1 40 | } 41 | 42 | $opt, $other, $err = getopt $Args 'v' 'verbose' 43 | if ($err) { "scoop alias: $err"; exit 1 } 44 | 45 | $name, $command, $description = $other 46 | $verbose = $opt.v -or $opt.verbose 47 | 48 | switch ($SubCommand) { 49 | 'add' { 50 | if (!$name -or !$command) { 51 | error " and must be specified for subcommand 'add'" 52 | exit 1 53 | } 54 | add_alias $name $command $description 55 | } 56 | 'rm' { 57 | if (!$name) { 58 | error " must be specified for subcommand 'rm'" 59 | exit 1 60 | } 61 | rm_alias $name 62 | } 63 | 'list' { 64 | list_aliases $verbose 65 | } 66 | } 67 | 68 | exit 0 69 | -------------------------------------------------------------------------------- /libexec/scoop-bucket.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop bucket add|list|known|rm [] 2 | # Summary: Manage Scoop buckets 3 | # Help: Add, list or remove buckets. 4 | # 5 | # Buckets are repositories of apps available to install. Scoop comes with 6 | # a default bucket, but you can also add buckets that you or others have 7 | # published. 8 | # 9 | # To add a bucket: 10 | # scoop bucket add [] 11 | # 12 | # e.g.: 13 | # scoop bucket add extras https://github.com/ScoopInstaller/Extras.git 14 | # 15 | # Since the 'extras' bucket is known to Scoop, this can be shortened to: 16 | # scoop bucket add extras 17 | # 18 | # To list all known buckets, use: 19 | # scoop bucket known 20 | param($cmd, $name, $repo) 21 | 22 | if (get_config USE_SQLITE_CACHE) { 23 | . "$PSScriptRoot\..\lib\manifest.ps1" 24 | . "$PSScriptRoot\..\lib\database.ps1" 25 | } 26 | 27 | $usage_add = 'usage: scoop bucket add []' 28 | $usage_rm = 'usage: scoop bucket rm ' 29 | 30 | switch ($cmd) { 31 | 'add' { 32 | if (!$name) { 33 | ' missing' 34 | $usage_add 35 | exit 1 36 | } 37 | if (!$repo) { 38 | $repo = known_bucket_repo $name 39 | if (!$repo) { 40 | "Unknown bucket '$name'. Try specifying ." 41 | $usage_add 42 | exit 1 43 | } 44 | } 45 | $status = add_bucket $name $repo 46 | exit $status 47 | } 48 | 'rm' { 49 | if (!$name) { 50 | ' missing' 51 | $usage_rm 52 | exit 1 53 | } 54 | $status = rm_bucket $name 55 | exit $status 56 | } 57 | 'list' { 58 | $buckets = list_buckets 59 | if (!$buckets.Length) { 60 | warn "No bucket found. Please run 'scoop bucket add main' to add the default 'main' bucket." 61 | exit 2 62 | } else { 63 | $buckets 64 | exit 0 65 | } 66 | } 67 | 'known' { 68 | known_buckets 69 | exit 0 70 | } 71 | default { 72 | "scoop bucket: cmd '$cmd' not supported" 73 | my_usage 74 | exit 1 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /libexec/scoop-cache.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop cache show|rm [app(s)] 2 | # Summary: Show or clear the download cache 3 | # Help: Scoop caches downloads so you don't need to download the same files 4 | # when you uninstall and re-install the same version of an app. 5 | # 6 | # You can use 7 | # scoop cache show 8 | # to see what's in the cache, and 9 | # scoop cache rm to remove downloads for a specific app. 10 | # 11 | # To clear everything in your cache, use: 12 | # scoop cache rm * 13 | # You can also use the `-a/--all` switch in place of `*` here 14 | 15 | param($cmd) 16 | 17 | function cacheinfo($file) { 18 | $app, $version, $url = $file.Name -split '#' 19 | New-Object PSObject -Property @{ Name = $app; Version = $version; Length = $file.Length } 20 | } 21 | 22 | function cacheshow($app) { 23 | if (!$app -or $app -eq '*') { 24 | $app = '.*?' 25 | } else { 26 | $app = '(' + ($app -join '|') + ')' 27 | } 28 | $files = @(Get-ChildItem $cachedir | Where-Object -Property Name -Value "^$app#" -Match) 29 | $totalLength = ($files | Measure-Object -Property Length -Sum).Sum 30 | 31 | $files | ForEach-Object { cacheinfo $_ } | Select-Object Name, Version, Length 32 | 33 | Write-Host "Total: $($files.Length) $(pluralize $files.Length 'file' 'files'), $(filesize $totalLength)" -ForegroundColor Yellow 34 | } 35 | 36 | function cacheremove($app) { 37 | if (!$app) { 38 | 'ERROR: missing' 39 | my_usage 40 | exit 1 41 | } elseif ($app -eq '*' -or $app -eq '-a' -or $app -eq '--all') { 42 | $files = @(Get-ChildItem $cachedir) 43 | } else { 44 | $app = '(' + ($app -join '|') + ')' 45 | $files = @(Get-ChildItem $cachedir | Where-Object -Property Name -Value "^$app#" -Match) 46 | } 47 | $totalLength = ($files | Measure-Object -Property Length -Sum).Sum 48 | 49 | $files | ForEach-Object { 50 | $curr = cacheinfo $_ 51 | Write-Host "Removing $($_.Name)..." 52 | Remove-Item $_.FullName 53 | if(Test-Path "$cachedir\$($curr.Name).txt") { 54 | Remove-Item "$cachedir\$($curr.Name).txt" 55 | } 56 | } 57 | 58 | Write-Host "Deleted: $($files.Length) $(pluralize $files.Length 'file' 'files'), $(filesize $totalLength)" -ForegroundColor Yellow 59 | } 60 | 61 | switch($cmd) { 62 | 'rm' { 63 | cacheremove $Args 64 | } 65 | 'show' { 66 | cacheshow $Args 67 | } 68 | default { 69 | cacheshow (@($cmd) + $Args) 70 | } 71 | } 72 | 73 | exit 0 74 | -------------------------------------------------------------------------------- /libexec/scoop-cat.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop cat 2 | # Summary: Show content of specified manifest. 3 | # Help: Show content of specified manifest. 4 | # If configured, `bat` will be used to pretty-print the JSON. 5 | # See `cat_style` in `scoop help config` for further information. 6 | 7 | param($app) 8 | 9 | . "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson' 10 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 11 | 12 | if (!$app) { error ' missing'; my_usage; exit 1 } 13 | 14 | $null, $manifest, $bucket, $url = Get-Manifest $app 15 | 16 | if ($manifest) { 17 | $style = get_config CAT_STYLE 18 | if ($style) { 19 | $manifest | ConvertToPrettyJson | bat --no-paging --style $style --language json 20 | } else { 21 | $manifest | ConvertToPrettyJson 22 | } 23 | } else { 24 | abort "Couldn't find manifest for '$app'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." 25 | } 26 | 27 | exit $exitCode 28 | -------------------------------------------------------------------------------- /libexec/scoop-checkup.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop checkup 2 | # Summary: Check for potential problems 3 | # Help: Performs a series of diagnostic tests to try to identify things that may 4 | # cause problems with Scoop. 5 | 6 | . "$PSScriptRoot\..\lib\diagnostic.ps1" 7 | 8 | $issues = 0 9 | $defenderIssues = 0 10 | 11 | $adminPrivileges = ([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) 12 | 13 | if ($adminPrivileges -and $env:USERNAME -ne 'WDAGUtilityAccount') { 14 | $defenderIssues += !(check_windows_defender $false) 15 | $defenderIssues += !(check_windows_defender $true) 16 | } 17 | 18 | $issues += !(check_main_bucket) 19 | $issues += !(check_long_paths) 20 | $issues += !(Get-WindowsDeveloperModeStatus) 21 | 22 | if (!(Test-HelperInstalled -Helper 7zip) -and !(get_config USE_EXTERNAL_7ZIP)) { 23 | warn "'7-Zip' is not installed! It's required for unpacking most programs. Please Run 'scoop install 7zip'." 24 | $issues++ 25 | } 26 | 27 | if (!(Test-HelperInstalled -Helper Innounp)) { 28 | warn "'Inno Setup Unpacker' is not installed! It's required for unpacking InnoSetup files. Please run 'scoop install innounp'." 29 | $issues++ 30 | } 31 | 32 | if (!(Test-HelperInstalled -Helper Dark)) { 33 | warn "'dark' is not installed! It's required for unpacking installers created with the WiX Toolset. Please run 'scoop install dark' or 'scoop install wixtoolset'." 34 | $issues++ 35 | } 36 | 37 | $globaldir = New-Object System.IO.DriveInfo($globaldir) 38 | if ($globaldir.DriveFormat -ne 'NTFS') { 39 | error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP_GLOBAL or 'global_path' variable in '~/.config/scoop/config.json' to another Drive." 40 | $issues++ 41 | } 42 | 43 | $scoopdir = New-Object System.IO.DriveInfo($scoopdir) 44 | if ($scoopdir.DriveFormat -ne 'NTFS') { 45 | error "Scoop requires an NTFS volume to work! Please point `$env:SCOOP or 'root_path' variable in '~/.config/scoop/config.json' to another Drive." 46 | $issues++ 47 | } 48 | 49 | if ($issues) { 50 | warn "Found $issues potential $(pluralize $issues problem problems)." 51 | } elseif ($defenderIssues) { 52 | info "Found $defenderIssues performance $(pluralize $defenderIssues problem problems)." 53 | warn "Security is more important than performance, in most cases." 54 | } else { 55 | success "No problems identified!" 56 | } 57 | 58 | exit 0 59 | -------------------------------------------------------------------------------- /libexec/scoop-cleanup.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop cleanup [options] 2 | # Summary: Cleanup apps by removing old versions 3 | # Help: 'scoop cleanup' cleans Scoop apps by removing old versions. 4 | # 'scoop cleanup ' cleans up the old versions of that app if said versions exist. 5 | # 6 | # You can use '*' in place of or `-a`/`--all` switch to cleanup all apps. 7 | # 8 | # Options: 9 | # -a, --all Cleanup all apps (alternative to '*') 10 | # -g, --global Cleanup a globally installed app 11 | # -k, --cache Remove outdated download cache 12 | 13 | . "$PSScriptRoot\..\lib\getopt.ps1" 14 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Select-CurrentVersion' (indirectly) 15 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 16 | . "$PSScriptRoot\..\lib\install.ps1" # persist related 17 | 18 | $opt, $apps, $err = getopt $args 'agk' 'all', 'global', 'cache' 19 | if ($err) { "scoop cleanup: $err"; exit 1 } 20 | $global = $opt.g -or $opt.global 21 | $cache = $opt.k -or $opt.cache 22 | $all = $opt.a -or $opt.all 23 | 24 | if (!$apps -and !$all) { 'ERROR: missing'; my_usage; exit 1 } 25 | 26 | if ($global -and !(is_admin)) { 27 | 'ERROR: you need admin rights to cleanup global apps'; exit 1 28 | } 29 | 30 | function cleanup($app, $global, $verbose, $cache) { 31 | $current_version = Select-CurrentVersion -AppName $app -Global:$global 32 | if ($cache) { 33 | Remove-Item "$cachedir\$app#*" -Exclude "$app#$current_version#*" 34 | } 35 | $appDir = appdir $app $global 36 | $versions = Get-ChildItem $appDir -Name 37 | $versions = $versions | Where-Object { $current_version -ne $_ -and $_ -ne 'current' } 38 | if (!$versions) { 39 | if ($verbose) { success "$app is already clean" } 40 | return 41 | } 42 | 43 | Write-Host -f yellow "Removing $app`:" -NoNewline 44 | $versions | ForEach-Object { 45 | $version = $_ 46 | Write-Host " $version" -NoNewline 47 | $dir = versiondir $app $version $global 48 | # unlink all potential old link before doing recursive Remove-Item 49 | unlink_persist_data (installed_manifest $app $version $global) $dir 50 | Remove-Item $dir -ErrorAction Stop -Recurse -Force 51 | } 52 | $leftVersions = Get-ChildItem $appDir 53 | if ($leftVersions.Length -eq 1 -and $leftVersions.Name -eq 'current' -and $leftVersions.LinkType) { 54 | attrib $leftVersions.FullName -R /L 55 | Remove-Item $leftVersions.FullName -ErrorAction Stop -Force 56 | $leftVersions = $null 57 | } 58 | if (!$leftVersions) { 59 | Remove-Item $appDir -ErrorAction Stop -Force 60 | } 61 | Write-Host '' 62 | } 63 | 64 | if ($apps -or $all) { 65 | if ($apps -eq '*' -or $all) { 66 | $verbose = $false 67 | $apps = applist (installed_apps $false) $false 68 | if ($global) { 69 | $apps += applist (installed_apps $true) $true 70 | } 71 | } else { 72 | $verbose = $true 73 | $apps = Confirm-InstallationStatus $apps -Global:$global 74 | } 75 | 76 | # $apps is now a list of ($app, $global) tuples 77 | $apps | ForEach-Object { cleanup @_ $verbose $cache } 78 | 79 | if ($cache) { 80 | Remove-Item "$cachedir\*.download" -ErrorAction Ignore 81 | } 82 | 83 | if (!$verbose) { 84 | success 'Everything is shiny now!' 85 | } 86 | } 87 | 88 | exit 0 89 | -------------------------------------------------------------------------------- /libexec/scoop-create.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop create 2 | # Summary: Create a custom app manifest 3 | # Help: Create your own custom app manifest 4 | param($url) 5 | 6 | function create_manifest($url) { 7 | $manifest = new_manifest 8 | 9 | $manifest.url = $url 10 | 11 | $url_parts = $null 12 | try { 13 | $url_parts = parse_url $url 14 | } catch { 15 | abort "Error: $url is not a valid URL" 16 | } 17 | 18 | $name = choose_item $url_parts 'App name' 19 | $name = if ($name.Length -gt 0) { 20 | $name 21 | } else { 22 | file_name ($url_parts | Select-Object -Last 1) 23 | } 24 | 25 | $manifest.version = choose_item $url_parts 'Version' 26 | 27 | $manifest | ConvertTo-Json | Out-File -FilePath "$name.json" -Encoding ASCII 28 | $manifest_path = Join-Path $pwd "$name.json" 29 | Write-Host "Created '$manifest_path'." 30 | } 31 | 32 | function new_manifest() { 33 | @{ 'homepage' = ''; 'license' = ''; 'version' = ''; 'url' = ''; 34 | 'hash' = ''; 'extract_dir' = ''; 'bin' = ''; 'depends' = '' 35 | } 36 | } 37 | 38 | function file_name($segment) { 39 | $segment.substring(0, $segment.lastindexof('.')) 40 | } 41 | 42 | function parse_url($url) { 43 | $uri = New-Object Uri $url 44 | $uri.pathandquery.substring(1).split('/') 45 | } 46 | 47 | function choose_item($list, $query) { 48 | for ($i = 0; $i -lt $list.count; $i++) { 49 | $item = $list[$i] 50 | Write-Host "$($i + 1)) $item" 51 | } 52 | $sel = Read-Host $query 53 | 54 | if ($sel.trim() -match '^[0-9+]$') { 55 | return $list[$sel - 1] 56 | } 57 | 58 | $sel 59 | } 60 | 61 | if (!$url) { 62 | & "$PSScriptRoot\scoop-help.ps1" create 63 | } else { 64 | create_manifest $url 65 | } 66 | 67 | exit 0 68 | -------------------------------------------------------------------------------- /libexec/scoop-depends.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop depends 2 | # Summary: List dependencies for an app, in the order they'll be installed 3 | 4 | . "$PSScriptRoot\..\lib\getopt.ps1" 5 | . "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency' 6 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' (indirectly) 7 | 8 | $opt, $apps, $err = getopt $args 'a:' 'arch=' 9 | $app = $apps[0] 10 | 11 | if(!$app) { error ' missing'; my_usage; exit 1 } 12 | 13 | $architecture = Get-DefaultArchitecture 14 | try { 15 | $architecture = Format-ArchitectureString ($opt.a + $opt.arch) 16 | } catch { 17 | abort "ERROR: $_" 18 | } 19 | 20 | $deps = @() 21 | Get-Dependency $app $architecture | ForEach-Object { 22 | $dep = [ordered]@{} 23 | $dep.Source, $dep.Name = $_ -split '/' 24 | $deps += [PSCustomObject]$dep 25 | } 26 | $deps 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /libexec/scoop-download.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop download [options] 2 | # Summary: Download apps in the cache folder and verify hashes 3 | # Help: e.g. The usual way to download an app, without installing it (uses your local 'buckets'): 4 | # scoop download git 5 | # 6 | # To download a different version of the app 7 | # (note that this will auto-generate the manifest using current version): 8 | # scoop download gh@2.7.0 9 | # 10 | # To download an app from a manifest at a URL: 11 | # scoop download https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/runat.json 12 | # 13 | # To download an app from a manifest on your computer 14 | # scoop download path\to\app.json 15 | # 16 | # Options: 17 | # -f, --force Force download (overwrite cache) 18 | # -s, --skip-hash-check Skip hash verification (use with caution!) 19 | # -u, --no-update-scoop Don't update Scoop before downloading if it's outdated 20 | # -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it 21 | 22 | . "$PSScriptRoot\..\lib\getopt.ps1" 23 | . "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly) 24 | . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) 25 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 26 | . "$PSScriptRoot\..\lib\install.ps1" 27 | if (get_config USE_SQLITE_CACHE) { 28 | . "$PSScriptRoot\..\lib\database.ps1" 29 | } 30 | 31 | $opt, $apps, $err = getopt $args 'fsua:' 'force', 'skip-hash-check', 'no-update-scoop', 'arch=' 32 | if ($err) { error "scoop download: $err"; exit 1 } 33 | 34 | $check_hash = !($opt.s -or $opt.'skip-hash-check') 35 | $use_cache = !($opt.f -or $opt.force) 36 | $architecture = Get-DefaultArchitecture 37 | try { 38 | $architecture = Format-ArchitectureString ($opt.a + $opt.arch) 39 | } catch { 40 | abort "ERROR: $_" 41 | } 42 | 43 | if (!$apps) { error ' missing'; my_usage; exit 1 } 44 | 45 | if (is_scoop_outdated) { 46 | if ($opt.u -or $opt.'no-update-scoop') { 47 | warn "Scoop is out of date." 48 | } else { 49 | & "$PSScriptRoot\scoop-update.ps1" 50 | } 51 | } 52 | 53 | # we only want to show this warning once 54 | if(!$use_cache) { warn "Cache is being ignored." } 55 | 56 | foreach ($curr_app in $apps) { 57 | # Prevent leaking variables from previous iteration 58 | $bucket = $version = $app = $manifest = $url = $null 59 | 60 | $app, $bucket, $version = parse_app $curr_app 61 | $app, $manifest, $bucket, $url = Get-Manifest "$bucket/$app" 62 | 63 | info "Downloading '$app'$(if ($version) { " ($version)" }) [$architecture]$(if ($bucket) { " from $bucket bucket" })" 64 | 65 | # Generate manifest if there is different version in manifest 66 | if (($null -ne $version) -and ($manifest.version -ne $version)) { 67 | $generated = generate_user_manifest $app $bucket $version 68 | if ($null -eq $generated) { 69 | error 'Manifest cannot be generated with provided version' 70 | continue 71 | } 72 | $manifest = parse_json($generated) 73 | } 74 | 75 | if(!$manifest) { 76 | error "Couldn't find manifest for '$app'$(if($bucket) { " from '$bucket' bucket" } elseif($url) { " at '$url'" })." 77 | continue 78 | } 79 | $version = $manifest.version 80 | if(!$version) { 81 | error "Manifest doesn't specify a version." 82 | continue 83 | } 84 | if($version -match '[^\w\.\-\+_]') { 85 | error "Manifest version has unsupported character '$($matches[0])'." 86 | continue 87 | } 88 | 89 | $curr_check_hash = $check_hash 90 | if ($version -eq 'nightly') { 91 | $version = nightly_version 92 | $curr_check_hash = $false 93 | } 94 | 95 | $architecture = Get-SupportedArchitecture $manifest $architecture 96 | if ($null -eq $architecture) { 97 | error "'$app' doesn't support current architecture!" 98 | continue 99 | } 100 | 101 | if(Test-Aria2Enabled) { 102 | Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash 103 | } else { 104 | foreach($url in script:url $manifest $architecture) { 105 | try { 106 | Invoke-CachedDownload $app $version $url $null $manifest.cookie $use_cache 107 | } catch { 108 | write-host -f darkred $_ 109 | error "URL $url is not valid" 110 | $dl_failure = $true 111 | continue 112 | } 113 | 114 | if($curr_check_hash) { 115 | $manifest_hash = hash_for_url $manifest $url $architecture 116 | $cached = cache_path $app $version $url 117 | $ok, $err = check_hash $cached $manifest_hash (show_app $app $bucket) 118 | 119 | if(!$ok) { 120 | error $err 121 | if(test-path $cached) { 122 | # rm cached file 123 | Remove-Item -force $cached 124 | } 125 | if ($url -like '*sourceforge.net*') { 126 | warn 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.' 127 | } 128 | error (new_issue_msg $app $bucket "hash check failed") 129 | continue 130 | } 131 | } else { 132 | info "Skipping hash verification." 133 | } 134 | } 135 | } 136 | 137 | if (!$dl_failure) { 138 | success "'$app' ($version) was downloaded successfully!" 139 | } 140 | } 141 | 142 | exit 0 143 | -------------------------------------------------------------------------------- /libexec/scoop-export.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop export > scoopfile.json 2 | # Summary: Exports installed apps, buckets (and optionally configs) in JSON format 3 | # Help: Options: 4 | # -c, --config Export the Scoop configuration file too 5 | 6 | . "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson' 7 | 8 | $export = @{} 9 | 10 | if ($args[0] -eq '-c' -or $args[0] -eq '--config') { 11 | $export.config = $scoopConfig 12 | # Remove machine-specific properties 13 | foreach ($prop in 'last_update', 'root_path', 'global_path', 'cache_path', 'alias') { 14 | $export.config.PSObject.Properties.Remove($prop) 15 | } 16 | } 17 | 18 | $export.buckets = list_buckets 19 | $export.apps = @(& "$PSScriptRoot\scoop-list.ps1" 6>$null) 20 | 21 | $export | ConvertToPrettyJSON 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /libexec/scoop-help.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop help 2 | # Summary: Show help for a command 3 | param($cmd) 4 | 5 | function print_help($cmd) { 6 | $file = Get-Content (command_path $cmd) -Raw 7 | 8 | $usage = usage $file 9 | $help = scoop_help $file 10 | 11 | if ($usage) { "$usage`n" } 12 | if ($help) { $help } 13 | } 14 | 15 | function print_summaries { 16 | $commands = @() 17 | 18 | command_files | ForEach-Object { 19 | $command = [ordered]@{} 20 | $command.Command = command_name $_ 21 | $command.Summary = summary (Get-Content (command_path $command.Command)) 22 | $commands += [PSCustomObject]$command 23 | } 24 | 25 | $commands 26 | } 27 | 28 | $commands = commands 29 | 30 | if(!($cmd)) { 31 | Write-Host "Usage: scoop [] 32 | 33 | Available commands are listed below. 34 | 35 | Type 'scoop help ' to get more help for a specific command." 36 | print_summaries 37 | } elseif($commands -contains $cmd) { 38 | print_help $cmd 39 | } else { 40 | warn "scoop help: no such command '$cmd'" 41 | exit 1 42 | } 43 | 44 | exit 0 45 | -------------------------------------------------------------------------------- /libexec/scoop-hold.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop hold 2 | # Summary: Hold an app to disable updates 3 | # Help: To hold a user-scoped app: 4 | # scoop hold 5 | # 6 | # To hold a global app: 7 | # scoop hold -g 8 | # 9 | # Options: 10 | # -g, --global Hold globally installed apps 11 | 12 | . "$PSScriptRoot\..\lib\getopt.ps1" 13 | . "$PSScriptRoot\..\lib\json.ps1" # 'save_install_info' (indirectly) 14 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'install_info' 'Select-CurrentVersion' (indirectly) 15 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 16 | 17 | $opt, $apps, $err = getopt $args 'g' 'global' 18 | if ($err) { "scoop hold: $err"; exit 1 } 19 | 20 | $global = $opt.g -or $opt.global 21 | 22 | if (!$apps) { 23 | my_usage 24 | exit 1 25 | } 26 | 27 | if ($global -and !(is_admin)) { 28 | error 'You need admin rights to hold a global app.' 29 | exit 1 30 | } 31 | 32 | foreach ($app in $apps) { 33 | 34 | if ($app -eq 'scoop') { 35 | $hold_update_until = [System.DateTime]::Now.AddDays(1) 36 | set_config HOLD_UPDATE_UNTIL $hold_update_until.ToString('o') | Out-Null 37 | success "$app is now held and might not be updated until $($hold_update_until.ToLocalTime())." 38 | continue 39 | } 40 | if (!(installed $app $global)) { 41 | if ($global) { 42 | error "'$app' is not installed globally." 43 | } else { 44 | error "'$app' is not installed." 45 | } 46 | continue 47 | } 48 | 49 | if (get_config NO_JUNCTION) { 50 | $version = Select-CurrentVersion -App $app -Global:$global 51 | } else { 52 | $version = 'current' 53 | } 54 | $dir = versiondir $app $version $global 55 | $json = install_info $app $version $global 56 | if (!$json) { 57 | error "Failed to hold '$app'." 58 | continue 59 | } 60 | $install = @{} 61 | $json | Get-Member -MemberType Properties | ForEach-Object { $install.Add($_.Name, $json.($_.Name)) } 62 | if ($install.hold) { 63 | info "'$app' is already held." 64 | continue 65 | } 66 | $install.hold = $true 67 | save_install_info $install $dir 68 | success "$app is now held and can not be updated anymore." 69 | } 70 | 71 | exit $exitcode 72 | -------------------------------------------------------------------------------- /libexec/scoop-home.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop home 2 | # Summary: Opens the app homepage 3 | param($app) 4 | 5 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 6 | 7 | if ($app) { 8 | $null, $manifest, $bucket, $null = Get-Manifest $app 9 | if ($manifest) { 10 | if ($manifest.homepage) { 11 | Start-Process $manifest.homepage 12 | } else { 13 | abort "Could not find homepage in manifest for '$app'." 14 | } 15 | } else { 16 | abort "Could not find manifest for '$app'." 17 | } 18 | } else { 19 | my_usage 20 | exit 1 21 | } 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /libexec/scoop-import.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop import 2 | # Summary: Imports apps, buckets and configs from a Scoopfile in JSON format 3 | # Help: To replicate a Scoop installation from a file stored on Desktop, run 4 | # scoop import Desktop\scoopfile.json 5 | 6 | param( 7 | [Parameter(Mandatory)] 8 | [String] 9 | $scoopfile 10 | ) 11 | 12 | . "$PSScriptRoot\..\lib\manifest.ps1" 13 | 14 | $import = $null 15 | $bucket_names = @() 16 | $def_arch = Get-DefaultArchitecture 17 | 18 | if (Test-Path $scoopfile) { 19 | $import = parse_json $scoopfile 20 | } elseif ($scoopfile -match '^(ht|f)tps?://|\\\\') { 21 | $import = url_manifest $scoopfile 22 | } 23 | 24 | if (!$import) { abort 'Input file not a valid JSON.' } 25 | 26 | foreach ($item in $import.config.PSObject.Properties) { 27 | set_config $item.Name $item.Value | Out-Null 28 | Write-Host "'$($item.Name)' has been set to '$($item.Value)'" 29 | } 30 | 31 | foreach ($item in $import.buckets) { 32 | add_bucket $item.Name $item.Source | Out-Null 33 | $bucket_names += $item.Name 34 | } 35 | 36 | foreach ($item in $import.apps) { 37 | $instArgs = @() 38 | $holdArgs = @() 39 | $info = $item.Info -Split ', ' 40 | if ('Global install' -in $info) { 41 | $instArgs += '--global' 42 | $holdArgs += '--global' 43 | } 44 | if ('64bit' -in $info -and '64bit' -ne $def_arch) { 45 | $instArgs += '--arch', '64bit' 46 | } elseif ('32bit' -in $info -and '32bit' -ne $def_arch) { 47 | $instArgs += '--arch', '32bit' 48 | } elseif ('arm64' -in $info -and 'arm64' -ne $def_arch) { 49 | $instArgs += '--arch', 'arm64' 50 | } 51 | 52 | $app = if ($item.Source -in $bucket_names) { 53 | "$($item.Source)/$($item.Name)" 54 | } elseif ($item.Source -eq '') { 55 | "$($item.Name)@$($item.Version)" 56 | } else { 57 | $item.Source 58 | } 59 | 60 | & "$PSScriptRoot\scoop-install.ps1" $app @instArgs 61 | 62 | if ('Held package' -in $info) { 63 | & "$PSScriptRoot\scoop-hold.ps1" $item.Name @holdArgs 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libexec/scoop-install.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop install [options] 2 | # Summary: Install apps 3 | # Help: e.g. The usual way to install an app (uses your local 'buckets'): 4 | # scoop install git 5 | # 6 | # To install a different version of the app 7 | # (note that this will auto-generate the manifest using current version): 8 | # scoop install gh@2.7.0 9 | # 10 | # To install an app from a manifest at a URL: 11 | # scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/runat.json 12 | # 13 | # To install a different version of the app from a URL: 14 | # scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/neovim.json@0.9.0 15 | # 16 | # To install an app from a manifest on your computer 17 | # scoop install \path\to\app.json 18 | # 19 | # To install an app from a manifest on your computer 20 | # scoop install \path\to\app.json@version 21 | # 22 | # Options: 23 | # -g, --global Install the app globally 24 | # -i, --independent Don't install dependencies automatically 25 | # -k, --no-cache Don't use the download cache 26 | # -s, --skip-hash-check Skip hash validation (use with caution!) 27 | # -u, --no-update-scoop Don't update Scoop before installing if it's outdated 28 | # -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it 29 | 30 | . "$PSScriptRoot\..\lib\getopt.ps1" 31 | . "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' 'manifest.ps1' (indirectly) 32 | . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) 33 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly) 34 | . "$PSScriptRoot\..\lib\system.ps1" 35 | . "$PSScriptRoot\..\lib\install.ps1" 36 | . "$PSScriptRoot\..\lib\decompress.ps1" 37 | . "$PSScriptRoot\..\lib\shortcuts.ps1" 38 | . "$PSScriptRoot\..\lib\psmodules.ps1" 39 | . "$PSScriptRoot\..\lib\versions.ps1" 40 | . "$PSScriptRoot\..\lib\depends.ps1" 41 | if (get_config USE_SQLITE_CACHE) { 42 | . "$PSScriptRoot\..\lib\database.ps1" 43 | } 44 | 45 | $opt, $apps, $err = getopt $args 'giksua:' 'global', 'independent', 'no-cache', 'skip-hash-check', 'no-update-scoop', 'arch=' 46 | if ($err) { "scoop install: $err"; exit 1 } 47 | 48 | $global = $opt.g -or $opt.global 49 | $check_hash = !($opt.s -or $opt.'skip-hash-check') 50 | $independent = $opt.i -or $opt.independent 51 | $use_cache = !($opt.k -or $opt.'no-cache') 52 | $architecture = Get-DefaultArchitecture 53 | try { 54 | $architecture = Format-ArchitectureString ($opt.a + $opt.arch) 55 | } catch { 56 | abort "ERROR: $_" 57 | } 58 | 59 | if (!$apps) { error ' missing'; my_usage; exit 1 } 60 | 61 | if ($global -and !(is_admin)) { 62 | abort 'ERROR: you need admin rights to install global apps' 63 | } 64 | 65 | if (is_scoop_outdated) { 66 | if ($opt.u -or $opt.'no-update-scoop') { 67 | warn "Scoop is out of date." 68 | } else { 69 | & "$PSScriptRoot\scoop-update.ps1" 70 | } 71 | } 72 | 73 | ensure_none_failed $apps 74 | 75 | if ($apps.length -eq 1) { 76 | $app, $null, $version = parse_app $apps 77 | if ($app.EndsWith('.json')) { 78 | $app = [System.IO.Path]::GetFileNameWithoutExtension($app) 79 | } 80 | $curVersion = Select-CurrentVersion -AppName $app -Global:$global 81 | if ($null -eq $version -and $curVersion) { 82 | warn "'$app' ($curVersion) is already installed.`nUse 'scoop update $app$(if ($global) { ' --global' })' to install a new version." 83 | exit 0 84 | } 85 | } 86 | 87 | # get any specific versions that we need to handle first 88 | $specific_versions = $apps | Where-Object { 89 | $null, $null, $version = parse_app $_ 90 | return $null -ne $version 91 | } 92 | 93 | # compare object does not like nulls 94 | if ($specific_versions.Count -gt 0) { 95 | $difference = Compare-Object -ReferenceObject $apps -DifferenceObject $specific_versions -PassThru 96 | } else { 97 | $difference = $apps 98 | } 99 | 100 | $specific_versions_paths = $specific_versions | ForEach-Object { 101 | $app, $bucket, $version = parse_app $_ 102 | if (installed_manifest $app $version) { 103 | warn "'$app' ($version) is already installed.`nUse 'scoop update $app$(if ($global) { ' --global' })' to install a new version." 104 | continue 105 | } 106 | 107 | generate_user_manifest $app $bucket $version 108 | } 109 | $apps = @((@($specific_versions_paths) + $difference) | Where-Object { $_ } | Select-Object -Unique) 110 | 111 | # remember which were explictly requested so that we can 112 | # differentiate after dependencies are added 113 | $explicit_apps = $apps 114 | 115 | if (!$independent) { 116 | $apps = $apps | Get-Dependency -Architecture $architecture | Select-Object -Unique # adds dependencies 117 | } 118 | ensure_none_failed $apps 119 | 120 | $apps, $skip = prune_installed $apps $global 121 | 122 | $skip | Where-Object { $explicit_apps -contains $_ } | ForEach-Object { 123 | $app, $null, $null = parse_app $_ 124 | $version = Select-CurrentVersion -AppName $app -Global:$global 125 | warn "'$app' ($version) is already installed. Skipping." 126 | } 127 | 128 | $suggested = @{ }; 129 | if ((Test-Aria2Enabled) -and (get_config 'aria2-warning-enabled' $true)) { 130 | warn "Scoop uses 'aria2c' for multi-connection downloads." 131 | warn "Should it cause issues, run 'scoop config aria2-enabled false' to disable it." 132 | warn "To disable this warning, run 'scoop config aria2-warning-enabled false'." 133 | } 134 | $apps | ForEach-Object { install_app $_ $architecture $global $suggested $use_cache $check_hash } 135 | 136 | show_suggestions $suggested 137 | 138 | exit 0 139 | -------------------------------------------------------------------------------- /libexec/scoop-list.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop list [query] 2 | # Summary: List installed apps 3 | # Help: Lists all installed apps, or the apps matching the supplied query. 4 | param($query) 5 | 6 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 7 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'parse_json' 'Select-CurrentVersion' (indirectly) 8 | 9 | $def_arch = Get-DefaultArchitecture 10 | if (-not (Get-FormatData ScoopApps)) { 11 | Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml" 12 | } 13 | 14 | $local = installed_apps $false | ForEach-Object { @{ name = $_ } } 15 | $global = installed_apps $true | ForEach-Object { @{ name = $_; global = $true } } 16 | 17 | $apps = @($local) + @($global) 18 | if (-not $apps) { 19 | Write-Host "There aren't any apps installed." 20 | exit 1 21 | } 22 | 23 | $list = @() 24 | Write-Host "Installed apps$(if($query) { `" matching '$query'`"}):" 25 | $apps | Where-Object { !$query -or ($_.name -match $query) } | ForEach-Object { 26 | $app = $_.name 27 | $global = $_.global 28 | $item = @{} 29 | $ver = Select-CurrentVersion -AppName $app -Global:$global 30 | $item.Name = $app 31 | $item.Version = $ver 32 | 33 | $install_info_path = "$(versiondir $app $ver $global)\install.json" 34 | $updated = (Get-Item (appdir $app $global)).LastWriteTime 35 | $install_info = $null 36 | if (Test-Path $install_info_path) { 37 | $install_info = parse_json $install_info_path 38 | $updated = (Get-Item $install_info_path).LastWriteTime 39 | } 40 | 41 | $item.Source = if ($install_info.bucket) { 42 | $install_info.bucket 43 | } elseif ($install_info.url) { 44 | if ($install_info.url -eq (usermanifest $app)) { '' } 45 | else { $install_info.url } 46 | } 47 | $item.Updated = $updated 48 | 49 | $info = @() 50 | if ($global) { $info += 'Global install' } 51 | if (failed $app $global) { $info += 'Install failed' } 52 | if ($install_info.hold) { $info += 'Held package' } 53 | if ($install_info.architecture -and $def_arch -ne $install_info.architecture) { 54 | $info += $install_info.architecture 55 | } 56 | $item.Info = $info -join ', ' 57 | 58 | $list += [PSCustomObject]$item 59 | } 60 | 61 | $list | Add-Member -TypeName 'ScoopApps' -PassThru 62 | exit 0 63 | -------------------------------------------------------------------------------- /libexec/scoop-prefix.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop prefix 2 | # Summary: Returns the path to the specified app 3 | param($app) 4 | 5 | . "$PSScriptRoot\..\lib\versions.ps1" # 'currentdir' (indirectly) 6 | 7 | if (!$app) { 8 | my_usage 9 | exit 1 10 | } 11 | 12 | $app_path = currentdir $app $false 13 | if (!(Test-Path $app_path)) { 14 | $app_path = currentdir $app $true 15 | } 16 | 17 | if (Test-Path $app_path) { 18 | Write-Output $app_path 19 | } else { 20 | abort "Could not find app path for '$app'." 21 | } 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /libexec/scoop-reset.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop reset 2 | # Summary: Reset an app to resolve conflicts 3 | # Help: Used to resolve conflicts in favor of a particular app. For example, 4 | # if you've installed 'python' and 'python27', you can use 'scoop reset' to switch between 5 | # using one or the other. 6 | # 7 | # You can use '*' in place of or `-a`/`--all` switch to reset all apps. 8 | 9 | . "$PSScriptRoot\..\lib\getopt.ps1" 10 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Select-CurrentVersion' (indirectly) 11 | . "$PSScriptRoot\..\lib\system.ps1" # 'env_add_path' (indirectly) 12 | . "$PSScriptRoot\..\lib\install.ps1" 13 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 14 | . "$PSScriptRoot\..\lib\shortcuts.ps1" 15 | 16 | $opt, $apps, $err = getopt $args 'a' 'all' 17 | if($err) { "scoop reset: $err"; exit 1 } 18 | $all = $opt.a -or $opt.all 19 | 20 | if(!$apps -and !$all) { error ' missing'; my_usage; exit 1 } 21 | 22 | if($apps -eq '*' -or $all) { 23 | $local = installed_apps $false | ForEach-Object { ,@($_, $false) } 24 | $global = installed_apps $true | ForEach-Object { ,@($_, $true) } 25 | $apps = @($local) + @($global) 26 | } 27 | 28 | $apps | ForEach-Object { 29 | ($app, $global) = $_ 30 | 31 | $app, $bucket, $version = parse_app $app 32 | 33 | if(($global -eq $null) -and (installed $app $true)) { 34 | # set global flag when running reset command on specific app 35 | $global = $true 36 | } 37 | 38 | if($app -eq 'scoop') { 39 | # skip scoop 40 | return 41 | } 42 | 43 | if(!(installed $app)) { 44 | error "'$app' isn't installed" 45 | return 46 | } 47 | 48 | if ($null -eq $version) { 49 | $version = Select-CurrentVersion -AppName $app -Global:$global 50 | } 51 | 52 | $manifest = installed_manifest $app $version $global 53 | # if this is null we know the version they're resetting to 54 | # is not installed 55 | if ($manifest -eq $null) { 56 | error "'$app ($version)' isn't installed" 57 | return 58 | } 59 | 60 | if($global -and !(is_admin)) { 61 | warn "'$app' ($version) is a global app. You need admin rights to reset it. Skipping." 62 | return 63 | } 64 | 65 | write-host "Resetting $app ($version)." 66 | 67 | $dir = Convert-Path (versiondir $app $version $global) 68 | $original_dir = $dir 69 | $persist_dir = persistdir $app $global 70 | 71 | #region Workaround for #2952 72 | if (test_running_process $app $global) { 73 | return 74 | } 75 | #endregion Workaround for #2952 76 | 77 | $install = install_info $app $version $global 78 | $architecture = $install.architecture 79 | 80 | $dir = link_current $dir 81 | create_shims $manifest $dir $global $architecture 82 | create_startmenu_shortcuts $manifest $dir $global $architecture 83 | # unset all potential old env before re-adding 84 | env_rm_path $manifest $dir $global $architecture 85 | env_rm $manifest $global $architecture 86 | env_add_path $manifest $dir $global $architecture 87 | env_set $manifest $global $architecture 88 | # unlink all potential old link before re-persisting 89 | unlink_persist_data $manifest $original_dir 90 | persist_data $manifest $original_dir $persist_dir 91 | persist_permission $manifest $global 92 | } 93 | 94 | exit 0 95 | -------------------------------------------------------------------------------- /libexec/scoop-status.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop status 2 | # Summary: Show status and check for new app versions 3 | # Help: Options: 4 | # -l, --local Checks the status for only the locally installed apps, 5 | # and disables remote fetching/checking for Scoop and buckets 6 | 7 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest' 'parse_json' "install_info" 8 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 9 | 10 | # check if scoop needs updating 11 | $currentdir = versiondir 'scoop' 'current' 12 | $needs_update = $false 13 | $bucket_needs_update = $false 14 | $script:network_failure = $false 15 | $no_remotes = $args[0] -eq '-l' -or $args[0] -eq '--local' 16 | if (!(Get-Command git -ErrorAction SilentlyContinue)) { $no_remotes = $true } 17 | $list = @() 18 | if (!(Get-FormatData ScoopStatus)) { 19 | Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml" 20 | } 21 | 22 | function Test-UpdateStatus($repopath) { 23 | if (Test-Path "$repopath\.git") { 24 | Invoke-Git -Path $repopath -ArgumentList @('fetch', '-q', 'origin') 25 | $script:network_failure = 128 -eq $LASTEXITCODE 26 | $branch = Invoke-Git -Path $repopath -ArgumentList @('branch', '--show-current') 27 | $commits = Invoke-Git -Path $repopath -ArgumentList @('log', "HEAD..origin/$branch", '--oneline') 28 | if ($commits) { return $true } 29 | else { return $false } 30 | } else { 31 | return $true 32 | } 33 | } 34 | 35 | if (!$no_remotes) { 36 | $needs_update = Test-UpdateStatus $currentdir 37 | foreach ($bucket in Get-LocalBucket) { 38 | if (Test-UpdateStatus (Find-BucketDirectory $bucket -Root)) { 39 | $bucket_needs_update = $true 40 | break 41 | } 42 | } 43 | } 44 | 45 | if ($needs_update) { 46 | warn "Scoop out of date. Run 'scoop update' to get the latest changes." 47 | } elseif ($bucket_needs_update) { 48 | warn "Scoop bucket(s) out of date. Run 'scoop update' to get the latest changes." 49 | } elseif (!$script:network_failure -and !$no_remotes) { 50 | success 'Scoop is up to date.' 51 | } 52 | 53 | $true, $false | ForEach-Object { # local and global apps 54 | $global = $_ 55 | $dir = appsdir $global 56 | if (!(Test-Path $dir)) { return } 57 | 58 | Get-ChildItem $dir | Where-Object name -NE 'scoop' | ForEach-Object { 59 | $app = $_.name 60 | $status = app_status $app $global 61 | if (!$status.outdated -and !$status.failed -and !$status.removed -and !$status.missing_deps) { return } 62 | 63 | $item = [ordered]@{} 64 | $item.Name = $app 65 | $item.'Installed Version' = $status.version 66 | $item.'Latest Version' = if ($status.outdated) { $status.latest_version } else { "" } 67 | $item.'Missing Dependencies' = $status.missing_deps -Split ' ' -Join ' | ' 68 | $info = @() 69 | if ($status.failed) { $info += 'Install failed' } 70 | if ($status.hold) { $info += 'Held package' } 71 | if ($status.removed) { $info += 'Manifest removed' } 72 | $item.Info = $info -join ', ' 73 | $list += [PSCustomObject]$item 74 | } 75 | } 76 | 77 | if ($list.Length -eq 0 -and !$needs_update -and !$bucket_needs_update -and !$script:network_failure) { 78 | success 'Everything is ok!' 79 | } 80 | 81 | $list | Add-Member -TypeName ScoopStatus -PassThru 82 | 83 | exit 0 84 | -------------------------------------------------------------------------------- /libexec/scoop-unhold.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop unhold 2 | # Summary: Unhold an app to enable updates 3 | # Help: To unhold a user-scoped app: 4 | # scoop unhold 5 | # 6 | # To unhold a global app: 7 | # scoop unhold -g 8 | # 9 | # Options: 10 | # -g, --global Unhold globally installed apps 11 | 12 | . "$PSScriptRoot\..\lib\getopt.ps1" 13 | . "$PSScriptRoot\..\lib\json.ps1" # 'save_install_info' (indirectly) 14 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'install_info' 'Select-CurrentVersion' (indirectly) 15 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 16 | 17 | $opt, $apps, $err = getopt $args 'g' 'global' 18 | if ($err) { "scoop unhold: $err"; exit 1 } 19 | 20 | $global = $opt.g -or $opt.global 21 | 22 | if (!$apps) { 23 | my_usage 24 | exit 1 25 | } 26 | 27 | if ($global -and !(is_admin)) { 28 | error 'You need admin rights to unhold a global app.' 29 | exit 1 30 | } 31 | 32 | $apps | ForEach-Object { 33 | $app = $_ 34 | 35 | if ($app -eq 'scoop') { 36 | set_config HOLD_UPDATE_UNTIL $null | Out-Null 37 | success "$app is no longer held and can be updated again." 38 | return 39 | } 40 | if (!(installed $app $global)) { 41 | if ($global) { 42 | error "'$app' is not installed globally." 43 | } else { 44 | error "'$app' is not installed." 45 | } 46 | return 47 | } 48 | 49 | if (get_config NO_JUNCTION){ 50 | $version = Select-CurrentVersion -App $app -Global:$global 51 | } else { 52 | $version = 'current' 53 | } 54 | $dir = versiondir $app $version $global 55 | $json = install_info $app $version $global 56 | if (!$json) { 57 | error "Failed to unhold '$app'" 58 | continue 59 | } 60 | $install = @{} 61 | $json | Get-Member -MemberType Properties | ForEach-Object { $install.Add($_.Name, $json.($_.Name)) } 62 | if (!$install.hold) { 63 | info "'$app' is not held." 64 | continue 65 | } 66 | $install.hold = $null 67 | save_install_info $install $dir 68 | success "$app is no longer held and can be updated again." 69 | } 70 | 71 | exit $exitcode 72 | -------------------------------------------------------------------------------- /libexec/scoop-uninstall.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop uninstall [options] 2 | # Summary: Uninstall an app 3 | # Help: e.g. scoop uninstall git 4 | # 5 | # Options: 6 | # -g, --global Uninstall a globally installed app 7 | # -p, --purge Remove all persistent data 8 | 9 | . "$PSScriptRoot\..\lib\getopt.ps1" 10 | . "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 'Select-CurrentVersion' (indirectly) 11 | . "$PSScriptRoot\..\lib\system.ps1" 12 | . "$PSScriptRoot\..\lib\install.ps1" 13 | . "$PSScriptRoot\..\lib\shortcuts.ps1" 14 | . "$PSScriptRoot\..\lib\psmodules.ps1" 15 | . "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion' 16 | 17 | # options 18 | $opt, $apps, $err = getopt $args 'gp' 'global', 'purge' 19 | 20 | if ($err) { 21 | error "scoop uninstall: $err" 22 | exit 1 23 | } 24 | 25 | $global = $opt.g -or $opt.global 26 | $purge = $opt.p -or $opt.purge 27 | 28 | if (!$apps) { 29 | error ' missing' 30 | my_usage 31 | exit 1 32 | } 33 | 34 | if ($global -and !(is_admin)) { 35 | error 'You need admin rights to uninstall global apps.' 36 | exit 1 37 | } 38 | 39 | if ($apps -eq 'scoop') { 40 | & "$PSScriptRoot\..\bin\uninstall.ps1" $global $purge 41 | exit 42 | } 43 | 44 | $apps = Confirm-InstallationStatus $apps -Global:$global 45 | if (!$apps) { exit 0 } 46 | 47 | :app_loop foreach ($_ in $apps) { 48 | ($app, $global) = $_ 49 | 50 | $version = Select-CurrentVersion -AppName $app -Global:$global 51 | $appDir = appdir $app $global 52 | if ($version) { 53 | Write-Host "Uninstalling '$app' ($version)." 54 | 55 | $dir = versiondir $app $version $global 56 | $persist_dir = persistdir $app $global 57 | 58 | $manifest = installed_manifest $app $version $global 59 | $install = install_info $app $version $global 60 | $architecture = $install.architecture 61 | 62 | Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture 63 | 64 | #region Workaround for #2952 65 | if (test_running_process $app $global) { 66 | continue 67 | } 68 | #endregion Workaround for #2952 69 | 70 | try { 71 | Test-Path $dir -ErrorAction Stop | Out-Null 72 | } catch [UnauthorizedAccessException] { 73 | error "Access denied: $dir. You might need to restart." 74 | continue 75 | } 76 | 77 | Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall 78 | rm_shims $app $manifest $global $architecture 79 | rm_startmenu_shortcuts $manifest $global $architecture 80 | 81 | # If a junction was used during install, that will have been used 82 | # as the reference directory. Otherwise it will just be the version 83 | # directory. 84 | $refdir = unlink_current $dir 85 | 86 | uninstall_psmodule $manifest $refdir $global 87 | 88 | env_rm_path $manifest $refdir $global $architecture 89 | env_rm $manifest $global $architecture 90 | 91 | try { 92 | # unlink all potential old link before doing recursive Remove-Item 93 | unlink_persist_data $manifest $dir 94 | Remove-Item $dir -Recurse -Force -ErrorAction Stop 95 | } catch { 96 | if (Test-Path $dir) { 97 | error "Couldn't remove '$(friendly_path $dir)'; it may be in use." 98 | continue 99 | } 100 | } 101 | 102 | Invoke-HookScript -HookType 'post_uninstall' -Manifest $manifest -Arch $architecture 103 | } 104 | # remove older versions 105 | $oldVersions = @(Get-ChildItem $appDir -Name -Exclude 'current') 106 | foreach ($version in $oldVersions) { 107 | Write-Host "Removing older version ($version)." 108 | $dir = versiondir $app $version $global 109 | try { 110 | # unlink all potential old link before doing recursive Remove-Item 111 | unlink_persist_data $manifest $dir 112 | Remove-Item $dir -Recurse -Force -ErrorAction Stop 113 | } catch { 114 | error "Couldn't remove '$(friendly_path $dir)'; it may be in use." 115 | continue app_loop 116 | } 117 | } 118 | if (Test-Path ($currentDir = Join-Path $appDir 'current')) { 119 | attrib $currentDir -R /L 120 | Remove-Item $currentDir -ErrorAction Stop -Force 121 | } 122 | if (!(Get-ChildItem $appDir)) { 123 | try { 124 | # if last install failed, the directory seems to be locked and this 125 | # will throw an error about the directory not existing 126 | Remove-Item $appdir -Recurse -Force -ErrorAction Stop 127 | } catch { 128 | if ((Test-Path $appdir)) { throw } # only throw if the dir still exists 129 | } 130 | } 131 | 132 | # purge persistant data 133 | if ($purge) { 134 | Write-Host 'Removing persisted data.' 135 | $persist_dir = persistdir $app $global 136 | 137 | if (Test-Path $persist_dir) { 138 | try { 139 | Remove-Item $persist_dir -Recurse -Force -ErrorAction Stop 140 | } catch { 141 | error "Couldn't remove '$(friendly_path $persist_dir)'; it may be in use." 142 | continue 143 | } 144 | } 145 | } 146 | 147 | success "'$app' was uninstalled." 148 | } 149 | 150 | exit 0 151 | -------------------------------------------------------------------------------- /libexec/scoop-which.ps1: -------------------------------------------------------------------------------- 1 | # Usage: scoop which 2 | # Summary: Locate a shim/executable (similar to 'which' on Linux) 3 | # Help: Locate the path to a shim/executable that was installed with Scoop (similar to 'which' on Linux) 4 | param($command) 5 | 6 | if (!$command) { 7 | error ' missing' 8 | my_usage 9 | exit 1 10 | } 11 | 12 | $path = Get-CommandPath $command 13 | 14 | if ($null -eq $path) { 15 | warn "'$command' not found, not a scoop shim, or a broken shim." 16 | exit 2 17 | } else { 18 | friendly_path $path 19 | exit 0 20 | } 21 | -------------------------------------------------------------------------------- /supporting/formats/ScoopTypes.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScoopAppsType 6 | 7 | ScoopApps 8 | 9 | 10 | 11 | 12 | 13 | 14 | Name 15 | 16 | 17 | Version 18 | 19 | 20 | Source 21 | 22 | 23 | Updated 24 | yyyy-MM-dd HH:mm:ss 25 | 26 | 27 | Info 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ScoopShimsType 36 | 37 | ScoopShims 38 | 39 | 40 | 41 | 42 | 43 | 44 | Name 45 | 46 | 47 | Source 48 | 49 | 50 | Alternatives 51 | 52 | 53 | IsGlobal 54 | 55 | 56 | IsHidden 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ScoopStatusType 65 | 66 | ScoopStatus 67 | 68 | 69 | 70 | 71 | 72 | 73 | Name 74 | 75 | 76 | Installed Version 77 | 78 | 79 | Latest Version 80 | 81 | 82 | Missing Dependencies 83 | 84 | 85 | Info 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /supporting/shims/71/checksum.sha256: -------------------------------------------------------------------------------- 1 | 70d4690b8ac3b3f715f537cdea6e07a39fda4bc0347bf6b958e4f3ff2f0e04d4 shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/71/checksum.sha512: -------------------------------------------------------------------------------- 1 | ecde07b32192846c4885cf4d2208eedc170765ea115ae49b81509fed0ce474e21064100bb2f3d815ee79f1c12463d32ef013d4182647eae71855cd18e4196176 shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/71/shim.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/shims/71/shim.exe -------------------------------------------------------------------------------- /supporting/shims/kiennq/checksum.sha256: -------------------------------------------------------------------------------- 1 | 410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/kiennq/checksum.sha512: -------------------------------------------------------------------------------- 1 | 9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/kiennq/shim.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/shims/kiennq/shim.exe -------------------------------------------------------------------------------- /supporting/shims/kiennq/version.txt: -------------------------------------------------------------------------------- 1 | v3.1.1 2 | -------------------------------------------------------------------------------- /supporting/shims/scoopcs/checksum.sha256: -------------------------------------------------------------------------------- 1 | 0116068768fc992fc536738396b33db3dafe6b0cf0e6f54f6d1aa8b0331f3cec *shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/scoopcs/checksum.sha512: -------------------------------------------------------------------------------- 1 | d734c528e9f20581ed3c7aa71a458f7dff7e2780fa0c319ccb9c813cd8dbf656bd7e550b81d2aa3ee8775bff9a4e507bc0b25f075697405adca0f47d37835848 *shim.exe 2 | -------------------------------------------------------------------------------- /supporting/shims/scoopcs/shim.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/shims/scoopcs/shim.exe -------------------------------------------------------------------------------- /supporting/shims/scoopcs/version.txt: -------------------------------------------------------------------------------- 1 | 1.1.0 2 | -------------------------------------------------------------------------------- /supporting/validator/.gitignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | -------------------------------------------------------------------------------- /supporting/validator/Scoop.Validator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using Newtonsoft.Json.Schema; 8 | 9 | namespace Scoop 10 | { 11 | public class JsonParserException : Exception 12 | { 13 | public string FileName { get; set; } 14 | public JsonParserException(string file, string message) : base(message) { this.FileName = file; } 15 | public JsonParserException(string file, string message, Exception inner) : base(message, inner) { this.FileName = file; } 16 | } 17 | 18 | public class Validator 19 | { 20 | private bool CI { get; set; } 21 | public JSchema Schema { get; private set; } 22 | public FileInfo SchemaFile { get; private set; } 23 | public JObject Manifest { get; private set; } 24 | public FileInfo ManifestFile { get; private set; } 25 | public IList Errors { get; private set; } 26 | public string ErrorsAsString 27 | { 28 | get 29 | { 30 | return String.Join(System.Environment.NewLine, this.Errors); 31 | } 32 | } 33 | 34 | private JSchema ParseSchema(string file) 35 | { 36 | try 37 | { 38 | return JSchema.Parse(File.ReadAllText(file, System.Text.Encoding.UTF8)); 39 | } 40 | catch (Newtonsoft.Json.JsonReaderException e) 41 | { 42 | throw new JsonParserException(Path.GetFileName(file), e.Message, e); 43 | } 44 | catch (FileNotFoundException e) 45 | { 46 | throw e; 47 | } 48 | } 49 | 50 | private JObject ParseManifest(string file) 51 | { 52 | try 53 | { 54 | return JObject.Parse(File.ReadAllText(file, System.Text.Encoding.UTF8)); 55 | } 56 | catch (Newtonsoft.Json.JsonReaderException e) 57 | { 58 | throw new JsonParserException(Path.GetFileName(file), e.Message, e); 59 | } 60 | catch (FileNotFoundException e) 61 | { 62 | throw e; 63 | } 64 | } 65 | 66 | public Validator(string schemaFile) 67 | { 68 | this.SchemaFile = new FileInfo(schemaFile); 69 | this.Errors = new List(); 70 | } 71 | 72 | public Validator(string schemaFile, bool ci) 73 | { 74 | this.SchemaFile = new FileInfo(schemaFile); 75 | this.Errors = new List(); 76 | this.CI = ci; 77 | } 78 | 79 | public bool Validate(string file) 80 | { 81 | this.ManifestFile = new FileInfo(file); 82 | return this.Validate(); 83 | } 84 | 85 | public bool Validate() 86 | { 87 | if (!this.SchemaFile.Exists) 88 | { 89 | Console.WriteLine("ERROR: Please provide schema.json!"); 90 | return false; 91 | } 92 | if (!this.ManifestFile.Exists) 93 | { 94 | Console.WriteLine("ERROR: Please provide manifest.json!"); 95 | return false; 96 | } 97 | this.Errors.Clear(); 98 | try 99 | { 100 | if (this.Schema == null) 101 | { 102 | this.Schema = this.ParseSchema(this.SchemaFile.FullName); 103 | } 104 | this.Manifest = this.ParseManifest(this.ManifestFile.FullName); 105 | } 106 | catch (FileNotFoundException e) 107 | { 108 | this.Errors.Add(e.Message); 109 | } 110 | catch (JsonParserException e) 111 | { 112 | this.Errors.Add(String.Format("{0}{1}: {2}", (this.CI ? " [*] " : ""), e.FileName, e.Message)); 113 | } 114 | 115 | if (this.Schema == null || this.Manifest == null) 116 | return false; 117 | 118 | IList validationErrors = new List(); 119 | 120 | this.Manifest.IsValid(this.Schema, out validationErrors); 121 | 122 | if (validationErrors.Count == 0) 123 | { 124 | return true; 125 | } 126 | traverseErrors(validationErrors, this.CI ? 3 : 1); 127 | 128 | return (this.Errors.Count == 0); 129 | } 130 | 131 | public void traverseErrors(IList errors, int level = 1) { 132 | if(errors == null) { 133 | return; 134 | } 135 | foreach (ValidationError error in errors) 136 | { 137 | StringBuilder sb = new StringBuilder(); 138 | sb.Insert(sb.Length, " ", level * 2); 139 | sb.Insert(sb.Length, this.CI ? "[*] " : "- "); 140 | sb.AppendFormat("Error: {0}\n", error.Message); 141 | 142 | sb.Insert(sb.Length, " ", level * 2); 143 | sb.Insert(sb.Length, this.CI ? " [^] " : " "); 144 | sb.AppendFormat("Line: {0}:{1}:{2}\n", this.ManifestFile.FullName, error.LineNumber, error.LinePosition); 145 | 146 | sb.Insert(sb.Length, " ", level * 2); 147 | sb.Insert(sb.Length, this.CI ? " [^] " : " "); 148 | sb.AppendFormat("Path: {0}/{1}", error.SchemaId, error.ErrorType); 149 | 150 | if(!this.CI) { 151 | sb.Insert(sb.Length, "\n"); 152 | } 153 | 154 | this.Errors.Add(sb.ToString()); 155 | 156 | if(error.ChildErrors != null || error.ChildErrors.Count > 0) { 157 | traverseErrors(error.ChildErrors, level + 1); 158 | } 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /supporting/validator/bin/Newtonsoft.Json.Schema.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/validator/bin/Newtonsoft.Json.Schema.dll -------------------------------------------------------------------------------- /supporting/validator/bin/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/validator/bin/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /supporting/validator/bin/Scoop.Validator.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/validator/bin/Scoop.Validator.dll -------------------------------------------------------------------------------- /supporting/validator/bin/checksum.sha256: -------------------------------------------------------------------------------- 1 | e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d *Newtonsoft.Json.dll 2 | 7496d5349a123a6e3696085662b2ff17b156ccdb0e30e0c396ac72d2da36ce1c *Newtonsoft.Json.Schema.dll 3 | 83b1006443e8c340ca4c631614fc2ce0d5cb9a28c851e3b59724299f58b1397f *Scoop.Validator.dll 4 | 87f8f8db2202a3fbef6f431d0b7e20cec9d32095c441927402041f3c4076c1b6 *validator.exe 5 | -------------------------------------------------------------------------------- /supporting/validator/bin/checksum.sha512: -------------------------------------------------------------------------------- 1 | 56eb7f070929b239642dab729537dde2c2287bdb852ad9e80b5358c74b14bc2b2dded910d0e3b6304ea27eb587e5f19db0a92e1cbae6a70fb20b4ef05057e4ac *Newtonsoft.Json.dll 2 | 78b12beb1e67ac4f6efa0fcba57b4b34ea6a31d8b369934d6b6a6617386ef9939ea453ac262916e5857ce0359eb809424ea33c676a87a8fdfd77a59b2ce96db0 *Newtonsoft.Json.Schema.dll 3 | e9da4370aee4df47eedcf15d9749712eee513e5a9115b808617ddfcfde5bc47a0410edfb57508fcf51033c0be967611b2fd2c2ba944de7290c020cc67f77ac57 *Scoop.Validator.dll 4 | 58a0c37e98cac17822c7756bf6686a5fb74e711b8d986d13bd2f689f6b3b1f485fcd908d92cbc6a162a0e5974c2c5a43de57d15f1996be0aa405e41ec2ec8393 *validator.exe 5 | -------------------------------------------------------------------------------- /supporting/validator/bin/validator.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/supporting/validator/bin/validator.exe -------------------------------------------------------------------------------- /supporting/validator/build.ps1: -------------------------------------------------------------------------------- 1 | Param([Switch]$Fast) 2 | Push-Location $PSScriptRoot 3 | . "$PSScriptRoot\..\..\lib\core.ps1" 4 | . "$PSScriptRoot\..\..\lib\install.ps1" 5 | 6 | if (!$Fast) { 7 | Write-Host 'Install dependencies ...' 8 | & "$PSScriptRoot\install.ps1" 9 | } 10 | 11 | $output = "$PSScriptRoot\bin" 12 | if (!$Fast) { 13 | Get-ChildItem "$PSScriptRoot\packages\Newtonsoft.*\lib\net45\*.dll" -File | ForEach-Object { Copy-Item $_ $output } 14 | } 15 | Write-Output 'Compiling Scoop.Validator.cs ...' 16 | & "$PSScriptRoot\packages\Microsoft.Net.Compilers.Toolset\tasks\net472\csc.exe" -deterministic -platform:anycpu -nologo -optimize -target:library -reference:"$output\Newtonsoft.Json.dll" -reference:"$output\Newtonsoft.Json.Schema.dll" -out:"$output\Scoop.Validator.dll" Scoop.Validator.cs 17 | Write-Output 'Compiling validator.cs ...' 18 | & "$PSScriptRoot\packages\Microsoft.Net.Compilers.Toolset\tasks\net472\csc.exe" -deterministic -platform:anycpu -nologo -optimize -target:exe -reference:"$output\Scoop.Validator.dll" -reference:"$output\Newtonsoft.Json.dll" -reference:"$output\Newtonsoft.Json.Schema.dll" -out:"$output\validator.exe" validator.cs 19 | 20 | Write-Output 'Computing checksums ...' 21 | Remove-Item "$PSScriptRoot\bin\checksum.sha256" -ErrorAction Ignore 22 | Remove-Item "$PSScriptRoot\bin\checksum.sha512" -ErrorAction Ignore 23 | Get-ChildItem "$PSScriptRoot\bin\*" -Include *.exe, *.dll | ForEach-Object { 24 | "$((Get-FileHash -Path $_ -Algorithm SHA256).Hash.ToLower()) *$($_.Name)" | Out-File "$PSScriptRoot\bin\checksum.sha256" -Append -Encoding oem 25 | "$((Get-FileHash -Path $_ -Algorithm SHA512).Hash.ToLower()) *$($_.Name)" | Out-File "$PSScriptRoot\bin\checksum.sha512" -Append -Encoding oem 26 | } 27 | Pop-Location 28 | -------------------------------------------------------------------------------- /supporting/validator/install.ps1: -------------------------------------------------------------------------------- 1 | # https://github.com/edymtt/nugetstandalone 2 | $destinationFolder = "$PSScriptRoot\packages" 3 | if ((Test-Path -Path $destinationFolder)) { 4 | Remove-Item -Path $destinationFolder -Recurse | Out-Null 5 | } 6 | 7 | New-Item $destinationFolder -Type Directory | Out-Null 8 | nuget install packages.config -o $destinationFolder -ExcludeVersion 9 | -------------------------------------------------------------------------------- /supporting/validator/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /supporting/validator/update.ps1: -------------------------------------------------------------------------------- 1 | # https://github.com/edymtt/nugetstandalone 2 | $destinationFolder = "$PSScriptRoot\packages" 3 | if (!(Test-Path -Path $destinationFolder)) { 4 | Write-Host -f Red "Run .\install.ps1 first!" 5 | exit 1 6 | } 7 | 8 | nuget update packages.config -r $destinationFolder 9 | Remove-Item $destinationFolder -Force -Recurse | Out-Null 10 | nuget install packages.config -o $destinationFolder -ExcludeVersion 11 | -------------------------------------------------------------------------------- /supporting/validator/validator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Scoop 8 | { 9 | public class Program 10 | { 11 | public static int Main(string[] args) 12 | { 13 | bool ci = String.Format("{0}", Environment.GetEnvironmentVariable("CI")).ToLower() == "true"; 14 | bool valid = true; 15 | 16 | if (args.Length < 2) 17 | { 18 | Console.WriteLine("Usage: validator.exe [...]"); 19 | return 1; 20 | } 21 | 22 | 23 | IList manifests = args.ToList(); 24 | String schema = manifests.First(); 25 | manifests.RemoveAt(0); 26 | String combinedArgs = String.Join("", manifests); 27 | if(combinedArgs.Contains("*") || combinedArgs.Contains("?")) { 28 | try { 29 | var path = new Uri(Path.Combine(Directory.GetCurrentDirectory(), combinedArgs)).LocalPath; 30 | var drive = Path.GetPathRoot(path); 31 | var pattern = path.Replace(drive, ""); 32 | manifests = Directory.GetFiles(drive, pattern).ToList(); 33 | } catch (System.ArgumentException ex) { 34 | Console.WriteLine("Invalid path provided! ({0})", ex.Message); 35 | return 1; 36 | } 37 | } 38 | 39 | Scoop.Validator validator = new Scoop.Validator(schema, ci); 40 | foreach(var manifest in manifests) { 41 | if (validator.Validate(manifest)) 42 | { 43 | if(ci) { 44 | Console.WriteLine(" [+] {0} validates against the schema!", Path.GetFileName(manifest)); 45 | } else { 46 | Console.WriteLine("- {0} validates against the schema!", Path.GetFileName(manifest)); 47 | } 48 | } 49 | else 50 | { 51 | if(ci) { 52 | Console.WriteLine(" [-] {0} has {1} Error{2}!", Path.GetFileName(manifest), validator.Errors.Count, validator.Errors.Count > 1 ? "s" : ""); 53 | } else { 54 | Console.WriteLine("- {0} has {1} Error{2}!", Path.GetFileName(manifest), validator.Errors.Count, validator.Errors.Count > 1 ? "s" : ""); 55 | } 56 | valid = false; 57 | foreach (var error in validator.Errors) 58 | { 59 | Console.WriteLine(error); 60 | } 61 | } 62 | } 63 | 64 | return valid ? 0 : 1; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /supporting/validator/validator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | Debug 10 | AnyCPU 11 | {8EB9B38A-1BAB-4D89-B4CB-ACE5E7C1073B} 12 | Exe 13 | Scoop.Validator 14 | Scoop.Validator 15 | v4.5.0 16 | 512 17 | true 18 | 19 | 20 | 22 | packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll 23 | True 24 | 25 | 27 | packages\Newtonsoft.Json.Schema.4.0.1\lib\net45\Newtonsoft.Json.Schema.dll 28 | True 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | This project references NuGet package(s) that are missing on this computer. 50 | Enable NuGet Package Restore to download them. For more information, see 51 | http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/Import-Bucket-Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | #Requires -Modules @{ ModuleName = 'BuildHelpers'; ModuleVersion = '2.0.1' } 3 | #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.2.0' } 4 | param( 5 | [String] $BucketPath = $MyInvocation.PSScriptRoot 6 | ) 7 | 8 | . "$PSScriptRoot\Scoop-00File.Tests.ps1" -TestPath $BucketPath 9 | 10 | Describe 'Manifest validates against the schema' { 11 | BeforeDiscovery { 12 | $bucketDir = if (Test-Path "$BucketPath\bucket") { 13 | "$BucketPath\bucket" 14 | } else { 15 | $BucketPath 16 | } 17 | if ($env:CI -eq $true) { 18 | Set-BuildEnvironment -Force 19 | $manifestFiles = @(Get-GitChangedFile -Path $bucketDir -Include '*.json' -Commit $env:BHCommitHash) 20 | } else { 21 | $manifestFiles = (Get-ChildItem $bucketDir -Filter '*.json' -Recurse).FullName 22 | } 23 | } 24 | BeforeAll { 25 | Add-Type -Path "$PSScriptRoot\..\supporting\validator\bin\Scoop.Validator.dll" 26 | # Could not use backslash '\' in Linux/macOS for .NET object 'Scoop.Validator' 27 | $validator = New-Object Scoop.Validator("$PSScriptRoot/../schema.json", $true) 28 | $global:quotaExceeded = $false 29 | } 30 | It '<_>' -TestCases $manifestFiles { 31 | if ($global:quotaExceeded) { 32 | Set-ItResult -Skipped -Because 'Schema validation limit exceeded.' 33 | } else { 34 | $file = $_ # exception handling may overwrite $_ 35 | try { 36 | $validator.Validate($file) 37 | if ($validator.Errors.Count -gt 0) { 38 | Write-Host " [-] $_ has $($validator.Errors.Count) Error$(If($validator.Errors.Count -gt 1) { 's' })!" -ForegroundColor Red 39 | Write-Host $validator.ErrorsAsString -ForegroundColor Yellow 40 | } 41 | $validator.Errors.Count | Should -Be 0 42 | } catch { 43 | if ($_.Exception.Message -like '*The free-quota limit of 1000 schema validations per hour has been reached.*') { 44 | $global:quotaExceeded = $true 45 | Set-ItResult -Skipped -Because 'Schema validation limit exceeded.' 46 | } else { 47 | throw 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Scoop-00File.Tests.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [String] $TestPath = "$PSScriptRoot\.." 3 | ) 4 | 5 | BeforeDiscovery { 6 | $project_file_exclusions = @( 7 | '[\\/]\.git[\\/]', 8 | '\.sublime-workspace$', 9 | '\.DS_Store$', 10 | 'supporting(\\|/)validator(\\|/)packages(\\|/)*' 11 | ) 12 | $repo_files = (Get-ChildItem $TestPath -File -Recurse).FullName | 13 | Where-Object { $_ -inotmatch $($project_file_exclusions -join '|') } 14 | } 15 | 16 | Describe 'Code Syntax' -ForEach @(, $repo_files) -Tag 'File' { 17 | BeforeAll { 18 | $files = @( 19 | $_ | Where-Object { $_ -imatch '.(ps1|psm1)$' } 20 | ) 21 | function Test-PowerShellSyntax { 22 | # ref: http://powershell.org/wp/forums/topic/how-to-check-syntax-of-scripts-automatically @@ https://archive.is/xtSv6 23 | # originally created by Alexander Petrovskiy & Dave Wyatt 24 | [CmdletBinding()] 25 | param ( 26 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 27 | [string[]] 28 | $Path 29 | ) 30 | 31 | process { 32 | foreach ($scriptPath in $Path) { 33 | $contents = Get-Content -Path $scriptPath 34 | 35 | if ($null -eq $contents) { 36 | continue 37 | } 38 | 39 | $errors = $null 40 | $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) 41 | 42 | New-Object psobject -Property @{ 43 | Path = $scriptPath 44 | SyntaxErrorsFound = ($errors.Count -gt 0) 45 | } 46 | } 47 | } 48 | } 49 | 50 | } 51 | 52 | It 'PowerShell code files do not contain syntax errors' { 53 | $badFiles = @( 54 | foreach ($file in $files) { 55 | if ( (Test-PowerShellSyntax $file).SyntaxErrorsFound ) { 56 | $file 57 | } 58 | } 59 | ) 60 | 61 | if ($badFiles.Count -gt 0) { 62 | throw "The following files have syntax errors: `r`n`r`n$($badFiles -join "`r`n")" 63 | } 64 | } 65 | 66 | } 67 | 68 | Describe 'Style constraints for non-binary project files' -ForEach @(, $repo_files) -Tag 'File' { 69 | BeforeAll { 70 | $files = @( 71 | # gather all files except '*.exe', '*.zip', or any .git repository files 72 | $_ | 73 | Where-Object { $_ -inotmatch '(.exe|.zip|.dll)$' } | 74 | Where-Object { $_ -inotmatch '(unformatted)' } 75 | ) 76 | } 77 | 78 | It 'files do not contain leading UTF-8 BOM' { 79 | # UTF-8 BOM == 0xEF 0xBB 0xBF 80 | # see http://www.powershellmagazine.com/2012/12/17/pscxtip-how-to-determine-the-byte-order-mark-of-a-text-file @@ https://archive.is/RgT42 81 | # ref: http://poshcode.org/2153 @@ https://archive.is/sGnnu 82 | $badFiles = @( 83 | foreach ($file in $files) { 84 | if ((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) { 85 | # PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream' 86 | $content = ([char[]](Get-Content $file -AsByteStream -TotalCount 3) -join '') 87 | } else { 88 | $content = ([char[]](Get-Content $file -Encoding byte -TotalCount 3) -join '') 89 | } 90 | if ([regex]::match($content, '(?ms)^\xEF\xBB\xBF').success) { 91 | $file 92 | } 93 | } 94 | ) 95 | 96 | if ($badFiles.Count -gt 0) { 97 | throw "The following files have utf-8 BOM: `r`n`r`n$($badFiles -join "`r`n")" 98 | } 99 | } 100 | 101 | It 'files end with a newline' { 102 | $badFiles = @( 103 | foreach ($file in $files) { 104 | # Ignore previous TestResults.xml 105 | if ($file -match 'TestResults.xml') { 106 | continue 107 | } 108 | $string = [System.IO.File]::ReadAllText($file) 109 | if ($string.Length -gt 0 -and $string[-1] -ne "`n") { 110 | $file 111 | } 112 | } 113 | ) 114 | 115 | if ($badFiles.Count -gt 0) { 116 | throw "The following files do not end with a newline: `r`n`r`n$($badFiles -join "`r`n")" 117 | } 118 | } 119 | 120 | It 'file newlines are CRLF' { 121 | $badFiles = @( 122 | foreach ($file in $files) { 123 | $content = [System.IO.File]::ReadAllText($file) 124 | if (!$content) { 125 | throw "File contents are null: $($file)" 126 | } 127 | $lines = [regex]::split($content, '\r\n') 128 | $lineCount = $lines.Count 129 | 130 | for ($i = 0; $i -lt $lineCount; $i++) { 131 | if ( [regex]::match($lines[$i], '\r|\n').success ) { 132 | $file 133 | break 134 | } 135 | } 136 | } 137 | ) 138 | 139 | if ($badFiles.Count -gt 0) { 140 | throw "The following files have non-CRLF line endings: `r`n`r`n$($badFiles -join "`r`n")" 141 | } 142 | } 143 | 144 | It 'files have no lines containing trailing whitespace' { 145 | $badLines = @( 146 | foreach ($file in $files) { 147 | # Ignore previous TestResults.xml 148 | if ($file -match 'TestResults.xml') { 149 | continue 150 | } 151 | $lines = [System.IO.File]::ReadAllLines($file) 152 | $lineCount = $lines.Count 153 | 154 | for ($i = 0; $i -lt $lineCount; $i++) { 155 | if ($lines[$i] -match '\s+$') { 156 | 'File: {0}, Line: {1}' -f $file, ($i + 1) 157 | } 158 | } 159 | } 160 | ) 161 | 162 | if ($badLines.Count -gt 0) { 163 | throw "The following $($badLines.Count) lines contain trailing whitespace: `r`n`r`n$($badLines -join "`r`n")" 164 | } 165 | } 166 | 167 | It 'any leading whitespace consists only of spaces (excepting makefiles)' { 168 | $badLines = @( 169 | foreach ($file in $files) { 170 | if ($file -inotmatch '(^|.)makefile$') { 171 | $lines = [System.IO.File]::ReadAllLines($file) 172 | $lineCount = $lines.Count 173 | 174 | for ($i = 0; $i -lt $lineCount; $i++) { 175 | if ($lines[$i] -notmatch '^[ ]*(\S|$)') { 176 | 'File: {0}, Line: {1}' -f $file, ($i + 1) 177 | } 178 | } 179 | } 180 | } 181 | ) 182 | 183 | if ($badLines.Count -gt 0) { 184 | throw "The following $($badLines.Count) lines contain TABs within leading whitespace: `r`n`r`n$($badLines -join "`r`n")" 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /test/Scoop-00Linting.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'PSScriptAnalyzer' -Tag 'Linter' { 2 | BeforeDiscovery { 3 | $scriptDir = @('.', 'bin', 'lib', 'libexec', 'test') 4 | } 5 | 6 | BeforeAll { 7 | $lintSettings = "$PSScriptRoot\..\PSScriptAnalyzerSettings.psd1" 8 | } 9 | 10 | It 'PSScriptAnalyzerSettings.ps1 should exist' { 11 | $lintSettings | Should -Exist 12 | } 13 | 14 | Context 'Linting all *.psd1, *.psm1 and *.ps1 files' { 15 | BeforeEach { 16 | $analysis = Invoke-ScriptAnalyzer -Path "$PSScriptRoot\..\$_" -Settings $lintSettings 17 | } 18 | It 'Should pass: <_>' -TestCases $scriptDir { 19 | $analysis | Should -HaveCount 0 20 | if ($analysis) { 21 | foreach ($result in $analysis) { 22 | switch -wildCard ($result.ScriptName) { 23 | '*.psm1' { $type = 'Module' } 24 | '*.ps1' { $type = 'Script' } 25 | '*.psd1' { $type = 'Manifest' } 26 | } 27 | Write-Warning " [*] $($result.Severity): $($result.Message)" 28 | Write-Warning " $($result.RuleName) in $type`: $directory\$($result.ScriptName):$($result.Line)" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/Scoop-Commands.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\core.ps1" 4 | . "$PSScriptRoot\..\lib\commands.ps1" 5 | } 6 | 7 | Describe 'Manipulate Alias' -Tag 'Scoop' { 8 | BeforeAll { 9 | Mock shimdir { "$TestDrive\shims" } 10 | Mock set_config {} 11 | Mock get_config { @{} } 12 | 13 | $shimdir = shimdir 14 | ensure $shimdir 15 | } 16 | 17 | It 'Creates a new alias if it does not exist' { 18 | $alias_script = "$shimdir\scoop-rm.ps1" 19 | $alias_script | Should -Not -Exist 20 | 21 | add_alias 'rm' '"hello, world!"' 22 | & $alias_script | Should -Be 'hello, world!' 23 | } 24 | 25 | It 'Skips an existing alias' { 26 | $alias_script = "$shimdir\scoop-rm.ps1" 27 | Mock abort {} 28 | New-Item $alias_script -Type File -Force 29 | $alias_script | Should -Exist 30 | 31 | add_alias 'rm' '"test"' 32 | Should -Invoke -CommandName abort -Times 1 -ParameterFilter { $msg -eq "File 'scoop-rm.ps1' already exists in shims directory." } 33 | } 34 | 35 | It 'Removes an existing alias' { 36 | $alias_script = "$shimdir\scoop-rm.ps1" 37 | $alias_script | Should -Exist 38 | Mock get_config { @(@{'rm' = 'scoop-rm' }) } 39 | Mock info {} 40 | 41 | rm_alias 'rm' 42 | $alias_script | Should -Not -Exist 43 | Should -Invoke -CommandName info -Times 1 -ParameterFilter { $msg -eq "Removing alias 'rm'..." } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/Scoop-Config.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\core.ps1" 4 | } 5 | 6 | Describe 'config' -Tag 'Scoop' { 7 | BeforeAll { 8 | $configFile = [IO.Path]::GetTempFileName() 9 | $unicode = [Regex]::Unescape('\u4f60\u597d\u3053\u3093\u306b\u3061\u306f') # 你好こんにちは 10 | } 11 | 12 | AfterAll { 13 | Remove-Item -Path $configFile -Force 14 | } 15 | 16 | It 'load_cfg should return null if config file does not exist' { 17 | load_cfg $configFile | Should -Be $null 18 | } 19 | 20 | It 'set_config should be able to save typed values correctly' { 21 | # number 22 | $scoopConfig = set_config 'one' 1 23 | $scoopConfig.one | Should -BeExactly 1 24 | 25 | # boolean 26 | $scoopConfig = set_config 'two' $true 27 | $scoopConfig.two | Should -BeTrue 28 | $scoopConfig = set_config 'three' $false 29 | $scoopConfig.three | Should -BeFalse 30 | 31 | # underline key 32 | $scoopConfig = set_config 'under_line' 'four' 33 | $scoopConfig.under_line | Should -BeExactly 'four' 34 | 35 | # string 36 | $scoopConfig = set_config 'five' 'not null' 37 | 38 | # datetime 39 | $scoopConfig = set_config 'time' ([System.DateTime]::Parse('2019-03-18T15:22:09.3930000+00:00', $null, [System.Globalization.DateTimeStyles]::AdjustToUniversal)) 40 | $scoopConfig.time | Should -BeOfType [System.DateTime] 41 | 42 | # non-ASCII 43 | $scoopConfig = set_config 'unicode' $unicode 44 | $scoopConfig.unicode | Should -Be $unicode 45 | } 46 | 47 | It 'load_cfg should return PSObject if config file exist' { 48 | $scoopConfig = load_cfg $configFile 49 | $scoopConfig | Should -Not -BeNullOrEmpty 50 | $scoopConfig | Should -BeOfType [System.Management.Automation.PSObject] 51 | $scoopConfig.one | Should -BeExactly 1 52 | $scoopConfig.two | Should -BeTrue 53 | $scoopConfig.three | Should -BeFalse 54 | $scoopConfig.under_line | Should -BeExactly 'four' 55 | $scoopConfig.five | Should -Be 'not null' 56 | $scoopConfig.time | Should -BeOfType [System.DateTime] 57 | $scoopConfig.time | Should -Be ([System.DateTime]::Parse('2019-03-18T15:22:09.3930000+00:00', $null, [System.Globalization.DateTimeStyles]::AdjustToUniversal)) 58 | $scoopConfig.unicode | Should -Be $unicode 59 | } 60 | 61 | It 'get_config should return exactly the same values' { 62 | $scoopConfig = load_cfg $configFile 63 | (get_config 'one') | Should -BeExactly 1 64 | (get_config 'two') | Should -BeTrue 65 | (get_config 'three') | Should -BeFalse 66 | (get_config 'under_line') | Should -BeExactly 'four' 67 | (get_config 'five') | Should -Be 'not null' 68 | (get_config 'time') | Should -BeOfType [System.DateTime] 69 | (get_config 'time') | Should -Be ([System.DateTime]::Parse('2019-03-18T15:22:09.3930000+00:00', $null, [System.Globalization.DateTimeStyles]::AdjustToUniversal)) 70 | (get_config 'unicode') | Should -Be $unicode 71 | } 72 | 73 | It 'set_config should remove a value if being set to $null' { 74 | $scoopConfig = load_cfg $configFile 75 | $scoopConfig = set_config 'five' $null 76 | $scoopConfig.five | Should -BeNullOrEmpty 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Scoop-Depends.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\core.ps1" 4 | . "$PSScriptRoot\..\lib\depends.ps1" 5 | . "$PSScriptRoot\..\lib\buckets.ps1" 6 | . "$PSScriptRoot\..\lib\install.ps1" 7 | . "$PSScriptRoot\..\lib\manifest.ps1" 8 | } 9 | 10 | Describe 'Package Dependencies' -Tag 'Scoop' { 11 | Context 'Requirement function' { 12 | It 'Test 7zip requirement' { 13 | Test-7zipRequirement -Uri 'test.xz' | Should -BeTrue 14 | Test-7zipRequirement -Uri 'test.bin' | Should -BeFalse 15 | Test-7zipRequirement -Uri @('test.xz', 'test.bin') | Should -BeTrue 16 | } 17 | It 'Test lessmsi requirement' { 18 | Mock get_config { $true } 19 | Test-LessmsiRequirement -Uri 'test.msi' | Should -BeTrue 20 | Test-LessmsiRequirement -Uri 'test.bin' | Should -BeFalse 21 | Test-LessmsiRequirement -Uri @('test.msi', 'test.bin') | Should -BeTrue 22 | } 23 | It 'Allow $Uri be $null' { 24 | Test-7zipRequirement -Uri $null | Should -BeFalse 25 | Test-LessmsiRequirement -Uri $null | Should -BeFalse 26 | } 27 | } 28 | 29 | Context 'InstallationHelper function' { 30 | BeforeAll { 31 | $working_dir = setup_working 'format/formatted' 32 | $manifest1 = parse_json (Join-Path $working_dir '3-array-with-single-and-multi.json') 33 | $manifest2 = parse_json (Join-Path $working_dir '4-script-block.json') 34 | Mock Test-HelperInstalled { $false } 35 | } 36 | It 'Get helpers from URL' { 37 | Mock get_config { $true } 38 | Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -Be @('lessmsi') 39 | } 40 | It 'Get helpers from script' { 41 | Mock get_config { $false } 42 | Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -Be @('7zip') 43 | } 44 | It 'Helpers reflect config changes' { 45 | Mock get_config { $false } -ParameterFilter { $name -eq 'USE_LESSMSI' } 46 | Mock get_config { $true } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' } 47 | Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -BeNullOrEmpty 48 | Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -BeNullOrEmpty 49 | } 50 | It 'Not return installed helpers' { 51 | Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' } 52 | Mock get_config { $false } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' } 53 | Mock Test-HelperInstalled { $true }-ParameterFilter { $Helper -eq '7zip' } 54 | Mock Test-HelperInstalled { $false }-ParameterFilter { $Helper -eq 'Lessmsi' } 55 | Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -Be @('lessmsi') 56 | Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -BeNullOrEmpty 57 | Mock Test-HelperInstalled { $false }-ParameterFilter { $Helper -eq '7zip' } 58 | Mock Test-HelperInstalled { $true }-ParameterFilter { $Helper -eq 'Lessmsi' } 59 | Get-InstallationHelper -Manifest $manifest1 -Architecture '32bit' | Should -BeNullOrEmpty 60 | Get-InstallationHelper -Manifest $manifest2 -Architecture '32bit' | Should -Be @('7zip') 61 | } 62 | } 63 | 64 | Context 'Dependencies resolution' { 65 | BeforeAll { 66 | Mock Test-HelperInstalled { $false } 67 | Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' } 68 | Mock Get-Manifest { 'lessmsi', @{}, $null, $null } -ParameterFilter { $app -eq 'lessmsi' } 69 | Mock Get-Manifest { '7zip', @{ url = 'test.msi' }, $null, $null } -ParameterFilter { $app -eq '7zip' } 70 | Mock Get-Manifest { 'innounp', @{}, $null, $null } -ParameterFilter { $app -eq 'innounp' } 71 | } 72 | 73 | It 'Resolve install dependencies' { 74 | Mock Get-Manifest { 'test', @{ url = 'test.7z' }, $null, $null } 75 | Get-Dependency -AppName 'test' -Architecture '32bit' | Should -Be @('lessmsi', '7zip', 'test') 76 | Mock Get-Manifest { 'test', @{ innosetup = $true }, $null, $null } 77 | Get-Dependency -AppName 'test' -Architecture '32bit' | Should -Be @('innounp', 'test') 78 | } 79 | It 'Resolve script dependencies' { 80 | Mock Get-Manifest { 'test', @{ pre_install = 'Expand-7zipArchive ' }, $null, $null } 81 | Get-Dependency -AppName 'test' -Architecture '32bit' | Should -Be @('lessmsi', '7zip', 'test') 82 | } 83 | It 'Resolve runtime dependencies' { 84 | Mock Get-Manifest { 'depends', @{}, $null, $null } -ParameterFilter { $app -eq 'depends' } 85 | Mock Get-Manifest { 'test', @{ depends = 'depends' }, $null, $null } 86 | Get-Dependency -AppName 'test' -Architecture '32bit' | Should -Be @('depends', 'test') 87 | } 88 | It 'Keep bucket name of app' { 89 | Mock Get-Manifest { 'depends', @{}, 'anotherbucket', $null } -ParameterFilter { $app -eq 'anotherbucket/depends' } 90 | Mock Get-Manifest { 'test', @{ depends = 'anotherbucket/depends' }, 'bucket', $null } 91 | Get-Dependency -AppName 'bucket/test' -Architecture '32bit' | Should -Be @('anotherbucket/depends', 'bucket/test') 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/Scoop-GetOpts.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\getopt.ps1" 4 | } 5 | 6 | Describe 'getopt' -Tag 'Scoop' { 7 | It 'handle short option with required argument missing' { 8 | $null, $null, $err = getopt '-x' 'x:' '' 9 | $err | Should -Be 'Option -x requires an argument.' 10 | 11 | $null, $null, $err = getopt '-xy' 'x:y' '' 12 | $err | Should -Be 'Option -x requires an argument.' 13 | } 14 | 15 | It 'handle long option with required argument missing' { 16 | $null, $null, $err = getopt '--arb' '' 'arb=' 17 | $err | Should -Be 'Option --arb requires an argument.' 18 | } 19 | 20 | It 'handle space in quote' { 21 | $opt, $rem, $err = getopt '-x', 'space arg' 'x:' '' 22 | $err | Should -BeNullOrEmpty 23 | $opt.x | Should -Be 'space arg' 24 | } 25 | 26 | It 'handle unrecognized short option' { 27 | $null, $null, $err = getopt '-az' 'a' '' 28 | $err | Should -Be 'Option -z not recognized.' 29 | } 30 | 31 | It 'handle unrecognized long option' { 32 | $null, $null, $err = getopt '--non-exist' '' '' 33 | $err | Should -Be 'Option --non-exist not recognized.' 34 | 35 | $null, $null, $err = getopt '--global', '--another' 'abc:de:' 'global', 'one' 36 | $err | Should -Be 'Option --another not recognized.' 37 | } 38 | 39 | It 'remaining args returned' { 40 | $opt, $rem, $err = getopt '-g', 'rem' 'g' '' 41 | $err | Should -BeNullOrEmpty 42 | $opt.g | Should -BeTrue 43 | $rem | Should -Not -BeNullOrEmpty 44 | $rem.length | Should -Be 1 45 | $rem[0] | Should -Be 'rem' 46 | } 47 | 48 | It 'get a long flag and a short option with argument' { 49 | $a = '--global -a 32bit test' -split ' ' 50 | $opt, $rem, $err = getopt $a 'ga:' 'global', 'arch=' 51 | 52 | $err | Should -BeNullOrEmpty 53 | $opt.global | Should -BeTrue 54 | $opt.a | Should -Be '32bit' 55 | } 56 | 57 | It 'handles regex characters' { 58 | $a = '-?' 59 | { $opt, $rem, $err = getopt $a 'ga:' 'global' 'arch=' } | Should -Not -Throw 60 | { $null, $null, $null = getopt $a '?:' 'help' | Should -Not -Throw } 61 | } 62 | 63 | It 'handles short option without required argument' { 64 | $null, $null, $err = getopt '-x' 'x' '' 65 | $err | Should -BeNullOrEmpty 66 | } 67 | 68 | It 'handles long option without required argument' { 69 | $opt, $null, $err = getopt '--long-arg' '' 'long-arg' 70 | $err | Should -BeNullOrEmpty 71 | $opt.'long-arg' | Should -BeTrue 72 | } 73 | 74 | It 'handles long option with required argument' { 75 | $opt, $null, $err = getopt '--long-arg', 'test' '' 'long-arg=' 76 | $err | Should -BeNullOrEmpty 77 | $opt.'long-arg' | Should -Be 'test' 78 | } 79 | 80 | It 'handles the option terminator' { 81 | $opt, $rem, $err = getopt '--long-arg', '--' '' 'long-arg' 82 | $err | Should -BeNullOrEmpty 83 | $opt.'long-arg' | Should -BeTrue 84 | $rem[0] | Should -BeNullOrEmpty 85 | $opt, $rem, $err = getopt '--long-arg', '--', '-x', '-y' 'xy' 'long-arg' 86 | $err | Should -BeNullOrEmpty 87 | $opt.'long-arg' | Should -BeTrue 88 | $opt.'x' | Should -BeNullOrEmpty 89 | $opt.'y' | Should -BeNullOrEmpty 90 | $rem[0] | Should -Be '-x' 91 | $rem[1] | Should -Be '-y' 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Scoop-Install.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\core.ps1" 4 | . "$PSScriptRoot\..\lib\system.ps1" 5 | . "$PSScriptRoot\..\lib\manifest.ps1" 6 | . "$PSScriptRoot\..\lib\install.ps1" 7 | } 8 | 9 | Describe 'appname_from_url' -Tag 'Scoop' { 10 | It 'should extract the correct name' { 11 | appname_from_url 'https://example.org/directory/foobar.json' | Should -Be 'foobar' 12 | } 13 | } 14 | 15 | Describe 'url_filename' -Tag 'Scoop' { 16 | It 'should extract the real filename from an url' { 17 | url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt' 18 | url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt' 19 | } 20 | 21 | It 'can be tricked with a hash to override the real filename' { 22 | url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip' 23 | } 24 | } 25 | 26 | Describe 'url_remote_filename' -Tag 'Scoop' { 27 | It 'should extract the real filename from an url' { 28 | url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt' 29 | url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt' 30 | } 31 | 32 | It 'can not be tricked with a hash to override the real filename' { 33 | url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip' 34 | } 35 | } 36 | 37 | Describe 'is_in_dir' -Tag 'Scoop', 'Windows' { 38 | It 'should work correctly' { 39 | is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse 40 | is_in_dir 'C:\test' 'C:\test\foo\baz.zip' | Should -BeTrue 41 | is_in_dir "$PSScriptRoot\..\" "$PSScriptRoot" | Should -BeFalse 42 | } 43 | } 44 | 45 | Describe 'env add and remove path' -Tag 'Scoop', 'Windows' { 46 | BeforeAll { 47 | # test data 48 | $manifest = @{ 49 | 'env_add_path' = @('foo', 'bar', '.', '..') 50 | } 51 | $testdir = Join-Path $PSScriptRoot 'path-test-directory' 52 | $global = $false 53 | } 54 | 55 | It 'should concat the correct path' { 56 | Mock Add-Path {} 57 | Mock Remove-Path {} 58 | 59 | # adding 60 | env_add_path $manifest $testdir $global 61 | Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" } 62 | Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" } 63 | Should -Invoke -CommandName Add-Path -Times 1 -ParameterFilter { $Path -like $testdir } 64 | Should -Invoke -CommandName Add-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot } 65 | 66 | env_rm_path $manifest $testdir $global 67 | Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\foo" } 68 | Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like "$testdir\bar" } 69 | Should -Invoke -CommandName Remove-Path -Times 1 -ParameterFilter { $Path -like $testdir } 70 | Should -Invoke -CommandName Remove-Path -Times 0 -ParameterFilter { $Path -like $PSScriptRoot } 71 | } 72 | } 73 | 74 | Describe 'shim_def' -Tag 'Scoop' { 75 | It 'should use strings correctly' { 76 | $target, $name, $shimArgs = shim_def 'command.exe' 77 | $target | Should -Be 'command.exe' 78 | $name | Should -Be 'command' 79 | $shimArgs | Should -BeNullOrEmpty 80 | } 81 | 82 | It 'should expand the array correctly' { 83 | $target, $name, $shimArgs = shim_def @('foo.exe', 'bar') 84 | $target | Should -Be 'foo.exe' 85 | $name | Should -Be 'bar' 86 | $shimArgs | Should -BeNullOrEmpty 87 | 88 | $target, $name, $shimArgs = shim_def @('foo.exe', 'bar', '--test') 89 | $target | Should -Be 'foo.exe' 90 | $name | Should -Be 'bar' 91 | $shimArgs | Should -Be '--test' 92 | } 93 | } 94 | 95 | Describe 'persist_def' -Tag 'Scoop' { 96 | It 'parses string correctly' { 97 | $source, $target = persist_def 'test' 98 | $source | Should -Be 'test' 99 | $target | Should -Be 'test' 100 | } 101 | 102 | It 'should handle sub-folder' { 103 | $source, $target = persist_def 'foo/bar' 104 | $source | Should -Be 'foo/bar' 105 | $target | Should -Be 'foo/bar' 106 | } 107 | 108 | It 'should handle arrays' { 109 | # both specified 110 | $source, $target = persist_def @('foo', 'bar') 111 | $source | Should -Be 'foo' 112 | $target | Should -Be 'bar' 113 | 114 | # only first specified 115 | $source, $target = persist_def @('foo') 116 | $source | Should -Be 'foo' 117 | $target | Should -Be 'foo' 118 | 119 | # null value specified 120 | $source, $target = persist_def @('foo', $null) 121 | $source | Should -Be 'foo' 122 | $target | Should -Be 'foo' 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/Scoop-Manifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\..\lib\json.ps1" 3 | . "$PSScriptRoot\..\lib\manifest.ps1" 4 | } 5 | 6 | Describe 'JSON parse and beautify' -Tag 'Scoop' { 7 | Context 'Parse JSON' { 8 | It 'success with valid json' { 9 | { parse_json "$PSScriptRoot\fixtures\manifest\wget.json" } | Should -Not -Throw 10 | } 11 | It 'fails with invalid json' { 12 | { parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Throw 13 | } 14 | } 15 | Context 'Beautify JSON' { 16 | BeforeDiscovery { 17 | $manifests = (Get-ChildItem "$PSScriptRoot\fixtures\format\formatted" -File -Filter '*.json').Name 18 | } 19 | BeforeAll { 20 | $format = "$PSScriptRoot\fixtures\format" 21 | } 22 | It '<_>' -ForEach $manifests { 23 | $pretty_json = (parse_json "$format\unformatted\$_") | ConvertToPrettyJson 24 | $correct = (Get-Content "$format\formatted\$_") -join "`r`n" 25 | $correct.CompareTo($pretty_json) | Should -Be 0 26 | } 27 | } 28 | } 29 | 30 | Describe 'Handle ARM64 and correctly fallback' -Tag 'Scoop' { 31 | It 'Should return "arm64" if supported' { 32 | $manifest1 = @{ url = 'test'; architecture = @{ 'arm64' = @{ pre_install = 'test' } } } 33 | $manifest2 = @{ url = 'test'; pre_install = "'arm64'" } 34 | $manifest3 = @{ architecture = @{ 'arm64' = @{ url = 'test' } } } 35 | Get-SupportedArchitecture $manifest1 'arm64' | Should -Be 'arm64' 36 | Get-SupportedArchitecture $manifest2 'arm64' | Should -Be 'arm64' 37 | Get-SupportedArchitecture $manifest3 'arm64' | Should -Be 'arm64' 38 | } 39 | It 'Should return "64bit" if unsupported on Windows 11' { 40 | $WindowsBuild = 22000 41 | $manifest1 = @{ url = 'test' } 42 | $manifest2 = @{ architecture = @{ '64bit' = @{ url = 'test' } } } 43 | Get-SupportedArchitecture $manifest1 'arm64' | Should -Be '64bit' 44 | Get-SupportedArchitecture $manifest2 'arm64' | Should -Be '64bit' 45 | } 46 | It 'Should return "32bit" if unsupported on Windows 10' { 47 | $WindowsBuild = 19044 48 | $manifest2 = @{ url = 'test' } 49 | $manifest1 = @{ url = 'test'; architecture = @{ '64bit' = @{ pre_install = 'test' } } } 50 | $manifest3 = @{ architecture = @{ '64bit' = @{ url = 'test' } } } 51 | Get-SupportedArchitecture $manifest1 'arm64' | Should -Be '32bit' 52 | Get-SupportedArchitecture $manifest2 'arm64' | Should -Be '32bit' 53 | Get-SupportedArchitecture $manifest3 'arm64' | Should -BeNullOrEmpty 54 | } 55 | } 56 | 57 | Describe 'Manifest Validator' -Tag 'Validator' { 58 | # Could not use backslash '\' in Linux/macOS for .NET object 'Scoop.Validator' 59 | BeforeAll { 60 | Add-Type -Path "$PSScriptRoot\..\supporting\validator\bin\Scoop.Validator.dll" 61 | $schema = "$PSScriptRoot/../schema.json" 62 | } 63 | 64 | It 'Scoop.Validator is available' { 65 | ([System.Management.Automation.PSTypeName]'Scoop.Validator').Type | Should -Be 'Scoop.Validator' 66 | } 67 | It 'fails with broken schema' { 68 | $validator = New-Object Scoop.Validator("$PSScriptRoot/fixtures/manifest/broken_schema.json", $true) 69 | $validator.Validate("$PSScriptRoot/fixtures/manifest/wget.json") | Should -BeFalse 70 | $validator.Errors.Count | Should -Be 1 71 | $validator.Errors | Select-Object -First 1 | Should -Match 'broken_schema.*(line 6).*(position 4)' 72 | } 73 | It 'fails with broken manifest' { 74 | $validator = New-Object Scoop.Validator($schema, $true) 75 | $validator.Validate("$PSScriptRoot/fixtures/manifest/broken_wget.json") | Should -BeFalse 76 | $validator.Errors.Count | Should -Be 1 77 | $validator.Errors | Select-Object -First 1 | Should -Match 'broken_wget.*(line 5).*(position 4)' 78 | } 79 | It 'fails with invalid manifest' { 80 | $validator = New-Object Scoop.Validator($schema, $true) 81 | $validator.Validate("$PSScriptRoot/fixtures/manifest/invalid_wget.json") | Should -BeFalse 82 | $validator.Errors.Count | Should -Be 16 83 | $validator.Errors | Select-Object -First 1 | Should -Match "Property 'randomproperty' has not been defined and the schema does not allow additional properties\." 84 | $validator.Errors | Select-Object -Last 1 | Should -Match 'Required properties are missing from object: version\.' 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/Scoop-TestLib.ps1: -------------------------------------------------------------------------------- 1 | # copies fixtures to a working directory 2 | function setup_working($name) { 3 | $fixtures = "$PSScriptRoot\fixtures\$name" 4 | if (!(Test-Path $fixtures)) { 5 | Write-Host "couldn't find fixtures for $name at $fixtures" -f red 6 | exit 1 7 | } 8 | 9 | # reset working dir 10 | $working_dir = "$([IO.Path]::GetTempPath())ScoopTestFixtures\$name" 11 | 12 | if (Test-Path $working_dir) { 13 | Remove-Item -Recurse -Force $working_dir 14 | } 15 | 16 | # set up 17 | Copy-Item $fixtures -Destination $working_dir -Recurse 18 | 19 | return $working_dir 20 | } 21 | -------------------------------------------------------------------------------- /test/Scoop-Versions.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | . "$PSScriptRoot\Scoop-TestLib.ps1" 3 | . "$PSScriptRoot\..\lib\versions.ps1" 4 | } 5 | 6 | Describe 'versions comparison' -Tag 'Scoop' { 7 | Context 'semver compliant versions' { 8 | It 'handles major.minor.patch progressing' { 9 | Compare-Version '0.1.0' '0.1.1' | Should -Be 1 10 | Compare-Version '0.1.1' '0.2.0' | Should -Be 1 11 | Compare-Version '0.2.0' '1.0.0' | Should -Be 1 12 | } 13 | 14 | It 'handles pre-release versioning progression' { 15 | Compare-Version '0.4.0' '0.5.0-alpha.1' | Should -Be 1 16 | Compare-Version '0.5.0-alpha.1' '0.5.0-alpha.2' | Should -Be 1 17 | Compare-Version '0.5.0-alpha.2' '0.5.0-alpha.10' | Should -Be 1 18 | Compare-Version '0.5.0-alpha.10' '0.5.0-beta' | Should -Be 1 19 | Compare-Version '0.5.0-beta' '0.5.0-alpha.10' | Should -Be -1 20 | Compare-Version '0.5.0-beta' '0.5.0-beta.0' | Should -Be 1 21 | } 22 | 23 | It 'handles the pre-release tags in an alphabetic order' { 24 | Compare-Version '0.5.0-rc.1' '0.5.0-z' | Should -Be 1 25 | Compare-Version '0.5.0-rc.1' '0.5.0-howdy' | Should -Be -1 26 | Compare-Version '0.5.0-howdy' '0.5.0-rc.1' | Should -Be 1 27 | } 28 | } 29 | 30 | Context 'semver semi-compliant versions' { 31 | It 'handles Windows-styled major.minor.patch.build progression' { 32 | Compare-Version '0.0.0.0' '0.0.0.1' | Should -Be 1 33 | Compare-Version '0.0.0.1' '0.0.0.2' | Should -Be 1 34 | Compare-Version '0.0.0.2' '0.0.1.0' | Should -Be 1 35 | Compare-Version '0.0.1.0' '0.0.1.1' | Should -Be 1 36 | Compare-Version '0.0.1.1' '0.0.1.2' | Should -Be 1 37 | Compare-Version '0.0.1.2' '0.0.2.0' | Should -Be 1 38 | Compare-Version '0.0.2.0' '0.1.0.0' | Should -Be 1 39 | Compare-Version '0.1.0.0' '0.1.0.1' | Should -Be 1 40 | Compare-Version '0.1.0.1' '0.1.0.2' | Should -Be 1 41 | Compare-Version '0.1.0.2' '0.1.1.0' | Should -Be 1 42 | Compare-Version '0.1.1.0' '0.1.1.1' | Should -Be 1 43 | Compare-Version '0.1.1.1' '0.1.1.2' | Should -Be 1 44 | Compare-Version '0.1.1.2' '0.2.0.0' | Should -Be 1 45 | Compare-Version '0.2.0.0' '1.0.0.0' | Should -Be 1 46 | } 47 | 48 | It 'handles partial semver version differences' { 49 | Compare-Version '1' '1.1' | Should -Be 1 50 | Compare-Version '1' '1.0' | Should -Be 1 51 | Compare-Version '1.1.0.0' '1.1' | Should -Be -1 52 | Compare-Version '1.4' '1.3.0' | Should -Be -1 53 | Compare-Version '1.4' '1.3.255.255' | Should -Be -1 54 | Compare-Version '1.4' '1.4.4' | Should -Be 1 55 | Compare-Version '1.1.1_8' '1.1.1' | Should -Be -1 56 | Compare-Version '1.1.1_8' '1.1.1_9' | Should -Be 1 57 | Compare-Version '1.1.1_10' '1.1.1_9' | Should -Be -1 58 | Compare-Version '1.1.1b' '1.1.1a' | Should -Be -1 59 | Compare-Version '1.1.1a' '1.1.1b' | Should -Be 1 60 | Compare-Version '1.1a2' '1.1a3' | Should -Be 1 61 | Compare-Version '1.1.1a10' '1.1.1b1' | Should -Be 1 62 | } 63 | 64 | It 'handles dash-style versions' { 65 | Compare-Version '1.8.9' '1.8.5-1' | Should -Be -1 66 | Compare-Version '7.0.4-9' '7.0.4-10' | Should -Be 1 67 | Compare-Version '7.0.4-9' '7.0.4-8' | Should -Be -1 68 | Compare-Version '2019-01-01' '2019-01-02' | Should -Be 1 69 | Compare-Version '2019-01-02' '2019-01-01' | Should -Be -1 70 | Compare-Version '2018-01-01' '2019-01-01' | Should -Be 1 71 | Compare-Version '2019-01-01' '2018-01-01' | Should -Be -1 72 | } 73 | It 'handles post-release tagging ("+")' { 74 | Compare-Version '1' '1+hotfix.0' | Should -Be 1 75 | Compare-Version '1.0.0' '1.0.0+hotfix.0' | Should -Be 1 76 | Compare-Version '1.0.0+hotfix.0' '1.0.0+hotfix.1' | Should -Be 1 77 | Compare-Version '1.0.0+hotfix.1' '1.0.1' | Should -Be 1 78 | Compare-Version '1.0.0+1.1' '1.0.0+1' | Should -Be -1 79 | } 80 | } 81 | 82 | Context 'other misc versions' { 83 | It 'handles plain text string' { 84 | Compare-Version 'latest' '20150405' | Should -Be -1 85 | Compare-Version '0.5alpha' '0.5' | Should -Be 1 86 | Compare-Version '0.5' '0.5Beta' | Should -Be -1 87 | Compare-Version '0.4' '0.5Beta' | Should -Be 1 88 | } 89 | 90 | It 'handles empty string' { 91 | Compare-Version '7.0.4-9' '' | Should -Be -1 92 | } 93 | 94 | It 'handles equal versions' { 95 | function get_config { $null } 96 | Compare-Version '12.0' '12.0' | Should -Be 0 97 | Compare-Version '7.0.4-9' '7.0.4-9' | Should -Be 0 98 | Compare-Version 'nightly-20190801' 'nightly' | Should -Be 0 99 | Compare-Version 'nightly-20190801' 'nightly-20200801' | Should -Be 0 100 | } 101 | 102 | It "handles nightly versions with 'update_nightly'" { 103 | function get_config { $true } 104 | Mock Get-Date { '20200801' } 105 | Compare-Version 'nightly-20200801' 'nightly' | Should -Be 0 106 | Compare-Version 'nightly-20200730' 'nightly' | Should -Be 1 107 | Compare-Version 'nightly-20200730' 'nightly-20200801' | Should -Be 1 108 | Compare-Version 'nightly-20200802' 'nightly' | Should -Be -1 109 | Compare-Version 'nightly-20200802' 'nightly-20200801' | Should -Be -1 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/bin/init.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | Write-Output "PowerShell: $($PSVersionTable.PSVersion)" 3 | Write-Output 'Check and install testsuite dependencies ...' 4 | if (Get-InstalledModule -Name Pester -MinimumVersion 5.2 -MaximumVersion 5.99 -ErrorAction SilentlyContinue) { 5 | Write-Output 'Pester 5 is already installed.' 6 | } else { 7 | Write-Output 'Installing Pester 5 ...' 8 | Install-Module -Repository PSGallery -Scope CurrentUser -Force -Name Pester -MinimumVersion 5.2 -MaximumVersion 5.99 -SkipPublisherCheck 9 | } 10 | if (Get-InstalledModule -Name PSScriptAnalyzer -MinimumVersion 1.17 -ErrorAction SilentlyContinue) { 11 | Write-Output 'PSScriptAnalyzer is already installed.' 12 | } else { 13 | Write-Output 'Installing PSScriptAnalyzer ...' 14 | Install-Module -Repository PSGallery -Scope CurrentUser -Force -Name PSScriptAnalyzer -SkipPublisherCheck 15 | } 16 | if (Get-InstalledModule -Name BuildHelpers -MinimumVersion 2.0 -ErrorAction SilentlyContinue) { 17 | Write-Output 'BuildHelpers is already installed.' 18 | } else { 19 | Write-Output 'Installing BuildHelpers ...' 20 | Install-Module -Repository PSGallery -Scope CurrentUser -Force -Name BuildHelpers -SkipPublisherCheck 21 | } 22 | -------------------------------------------------------------------------------- /test/bin/test.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | #Requires -Modules @{ ModuleName = 'BuildHelpers'; ModuleVersion = '2.0.1' } 3 | #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.2.0' } 4 | #Requires -Modules @{ ModuleName = 'PSScriptAnalyzer'; ModuleVersion = '1.17.1' } 5 | param( 6 | [String] $TestPath = (Convert-Path "$PSScriptRoot\..") 7 | ) 8 | 9 | $pesterConfig = New-PesterConfiguration -Hashtable @{ 10 | Run = @{ 11 | Path = $TestPath 12 | PassThru = $true 13 | } 14 | Output = @{ 15 | Verbosity = 'Detailed' 16 | } 17 | } 18 | $excludes = @() 19 | 20 | if ($IsLinux -or $IsMacOS) { 21 | Write-Warning 'Skipping Windows-only tests on Linux/macOS' 22 | $excludes += 'Windows' 23 | } 24 | 25 | if ($env:CI -eq $true) { 26 | Write-Host "Load 'BuildHelpers' environment variables ..." 27 | Set-BuildEnvironment -Force 28 | 29 | # Check if tests are called from the Core itself, if so, adding excludes 30 | if ($TestPath -eq (Convert-Path "$PSScriptRoot\..")) { 31 | if ($env:BHCommitMessage -match '!linter') { 32 | Write-Warning "Skipping code linting per commit flag '!linter'" 33 | $excludes += 'Linter' 34 | } 35 | 36 | $changedScripts = (Get-GitChangedFile -Include '*.ps1', '*.psd1', '*.psm1' -Commit $env:BHCommitHash) 37 | if (!$changedScripts) { 38 | Write-Warning "Skipping tests and code linting for PowerShell scripts because they didn't change" 39 | $excludes += 'Linter' 40 | $excludes += 'Scoop' 41 | } 42 | 43 | if (!($changedScripts -like '*decompress.ps1') -and !($changedScripts -like '*Decompress.Tests.ps1')) { 44 | Write-Warning "Skipping tests and code linting for decompress.ps1 files because it didn't change" 45 | $excludes += 'Decompress' 46 | } 47 | 48 | if ('Decompress' -notin $excludes -and 'Windows' -notin $excludes) { 49 | Write-Host 'Install decompress dependencies ...' 50 | 51 | Write-Host (7z.exe | Select-String -Pattern '7-Zip').ToString() 52 | 53 | $env:SCOOP_HELPERS_PATH = 'C:\projects\helpers' 54 | if (!(Test-Path $env:SCOOP_HELPERS_PATH)) { 55 | New-Item -ItemType Directory -Path $env:SCOOP_HELPERS_PATH | Out-Null 56 | } 57 | 58 | $env:SCOOP_LESSMSI_PATH = "$env:SCOOP_HELPERS_PATH\lessmsi\lessmsi.exe" 59 | if (!(Test-Path $env:SCOOP_LESSMSI_PATH)) { 60 | $source = 'https://github.com/activescott/lessmsi/releases/download/v1.10.0/lessmsi-v1.10.0.zip' 61 | $destination = "$env:SCOOP_HELPERS_PATH\lessmsi.zip" 62 | Invoke-WebRequest -Uri $source -OutFile $destination 63 | & 7z.exe x "$env:SCOOP_HELPERS_PATH\lessmsi.zip" -o"$env:SCOOP_HELPERS_PATH\lessmsi" -y | Out-Null 64 | } 65 | 66 | $env:SCOOP_INNOUNP_PATH = "$env:SCOOP_HELPERS_PATH\innounp\innounp.exe" 67 | if (!(Test-Path $env:SCOOP_INNOUNP_PATH)) { 68 | $source = 'https://raw.githubusercontent.com/ScoopInstaller/Binary/master/innounp/innounp050.rar' 69 | $destination = "$env:SCOOP_HELPERS_PATH\innounp.rar" 70 | Invoke-WebRequest -Uri $source -OutFile $destination 71 | & 7z.exe x "$env:SCOOP_HELPERS_PATH\innounp.rar" -o"$env:SCOOP_HELPERS_PATH\innounp" -y | Out-Null 72 | } 73 | } 74 | } 75 | 76 | # Display CI environment variables 77 | $buildVariables = (Get-ChildItem -Path 'Env:').Where({ $_.Name -match '^(?:BH|CI(?:_|$)|APPVEYOR|GITHUB_|RUNNER_|SCOOP_)' }) 78 | $details = $buildVariables | 79 | Where-Object -FilterScript { $_.Name -notmatch 'EMAIL' } | 80 | Sort-Object -Property 'Name' | 81 | Format-Table -AutoSize -Property 'Name', 'Value' | 82 | Out-String 83 | Write-Host 'CI variables:' 84 | Write-Host $details -ForegroundColor DarkGray 85 | } 86 | 87 | if ($excludes.Length -gt 0) { 88 | $pesterConfig.Filter.ExcludeTag = $excludes 89 | } 90 | 91 | if ($env:BHBuildSystem -eq 'AppVeyor') { 92 | # AppVeyor 93 | $resultsXml = "$PSScriptRoot\TestResults.xml" 94 | $pesterConfig.TestResult.Enabled = $true 95 | $pesterConfig.TestResult.OutputPath = $resultsXml 96 | $result = Invoke-Pester -Configuration $pesterConfig 97 | Add-TestResultToAppveyor -TestFile $resultsXml 98 | } else { 99 | # GitHub Actions / Local 100 | $result = Invoke-Pester -Configuration $pesterConfig 101 | } 102 | 103 | exit $result.FailedCount 104 | -------------------------------------------------------------------------------- /test/fixtures/decompress/TestCases.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScoopInstaller/Scoop/859d1db51bcc840903d5280567846ae2f7207ca2/test/fixtures/decompress/TestCases.zip -------------------------------------------------------------------------------- /test/fixtures/format/formatted/1-easy.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": "single" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/format/formatted/2-whitespaces-mess.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.5.18", 3 | "url": "https://whatever", 4 | "hash": "whatever", 5 | "architecture": { 6 | "64bit": { 7 | "installer": { 8 | "script": [ 9 | "Do something", 10 | "cosi" 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/format/formatted/3-array-with-single-and-multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "http://www.7-zip.org/", 3 | "description": "A multi-format file archiver with high compression ratios", 4 | "license": { 5 | "identifier": "LGPL-2.0-only,BSD-3-Clause", 6 | "url": "https://www.7-zip.org/license.txt" 7 | }, 8 | "version": "18.05", 9 | "architecture": { 10 | "64bit": { 11 | "url": "https://7-zip.org/a/7z1805-x64.msi", 12 | "hash": "898c1ca0015183fe2ba7d55cacf0a1dea35e873bf3f8090f362a6288c6ef08d7" 13 | }, 14 | "32bit": { 15 | "url": "https://7-zip.org/a/7z1805.msi", 16 | "hash": "c554238bee18a03d736525e06d9258c9ecf7f64ead7c6b0d1eb04db2c0de30d0" 17 | } 18 | }, 19 | "extract_dir": "Files/7-Zip", 20 | "bin": [ 21 | "single", 22 | [ 23 | "7z.exe", 24 | "cosi" 25 | ], 26 | [ 27 | "7z.exe", 28 | "cosi", 29 | "param", 30 | "icon" 31 | ], 32 | [ 33 | "7z.exe", 34 | "empty", 35 | "", 36 | "" 37 | ], 38 | "singtwo" 39 | ], 40 | "checkver": "Download 7-Zip ([\\d.]+)", 41 | "autoupdate": { 42 | "architecture": { 43 | "64bit": { 44 | "url": "https://7-zip.org/a/7z$cleanVersion-x64.msi" 45 | }, 46 | "32bit": { 47 | "url": "https://7-zip.org/a/7z$cleanVersion.msi" 48 | } 49 | } 50 | }, 51 | "shortcuts": [ 52 | [ 53 | "7zFM.exe", 54 | "7-Zip" 55 | ], 56 | [ 57 | "name with spaces.exe", 58 | "Shortcut with spaces in name" 59 | ] 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /test/fixtures/format/formatted/4-script-block.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.6", 3 | "description": "Rambox Pro. Free, Open Source and Cross Platform messaging and emailing app that combines common web applications into one.", 4 | "homepage": "https://rambox.pro/", 5 | "url": "https://github.com/ramboxapp/download/releases/download/v1.0.6/RamboxPro-1.0.6-win.exe#/cosi.7z", 6 | "hash": "sha512:f4a1b5e12ae15c9a1339fef56b0522b6619d6c23b0ab806f128841c2ba7ce9d9c997fea81f5bc4a24988aed672a4415ff353542535dc7869b5e496f2f1e1efff", 7 | "extract_dir": "\\$PLUGINSDIR", 8 | "pre_install": "Get-ChildItem \"$dir\" -Exclude 'app-64.7z', 'app-32.7z' | Remove-Item -Force -Recurse", 9 | "architecture": { 10 | "64bit": { 11 | "installer": { 12 | "script": "Expand-7zipArchive \"$dir\\app-64.7z\" \"$dir\"" 13 | } 14 | }, 15 | "32bit": { 16 | "installer": { 17 | "script": "Expand-7zipArchive \"$dir\\app-32.7z\" \"$dir\"" 18 | } 19 | } 20 | }, 21 | "post_install": "Remove-Item \"$dir\\app-64.7z\", \"$dir\\app-32.7z\"", 22 | "shortcuts": [ 23 | [ 24 | "RamboxPro.exe", 25 | "RamboxPro" 26 | ] 27 | ], 28 | "checkver": { 29 | "github": "https://github.com/ramboxapp/download/" 30 | }, 31 | "autoupdate": { 32 | "url": "https://github.com/ramboxapp/download/releases/download/v$version/RamboxPro-$version-win.exe#/cosi.7z", 33 | "hash": { 34 | "url": "https://github.com/ramboxapp/download/releases/download/v$version/latest.yml", 35 | "find": "sha512:\\s+(.*)" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/format/unformatted/1-easy.json: -------------------------------------------------------------------------------- 1 | { "bin": ["single"]} 2 | -------------------------------------------------------------------------------- /test/fixtures/format/unformatted/2-whitespaces-mess.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.5.18", 3 | "url": "https://whatever", 4 | 5 | "hash": "whatever", 6 | "architecture": { 7 | "64bit": { "installer": { 8 | 9 | "script": [ 10 | "Do something" , "cosi" 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /test/fixtures/format/unformatted/3-array-with-single-and-multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "http://www.7-zip.org/", 3 | "description": "A multi-format file archiver with high compression ratios", 4 | "license": { 5 | "identifier": "LGPL-2.0-only,BSD-3-Clause", 6 | "url": "https://www.7-zip.org/license.txt" 7 | }, 8 | "version": "18.05", 9 | "architecture": { 10 | "64bit": { 11 | "url": "https://7-zip.org/a/7z1805-x64.msi", 12 | "hash": [ 13 | "898c1ca0015183fe2ba7d55cacf0a1dea35e873bf3f8090f362a6288c6ef08d7" 14 | ] 15 | }, 16 | "32bit": { 17 | "url": "https://7-zip.org/a/7z1805.msi", 18 | "hash": "c554238bee18a03d736525e06d9258c9ecf7f64ead7c6b0d1eb04db2c0de30d0" 19 | } 20 | }, 21 | "extract_dir": "Files/7-Zip", 22 | "bin": [ 23 | [ 24 | "single" 25 | ], 26 | [ 27 | "7z.exe", 28 | "cosi" 29 | ], 30 | [ 31 | "7z.exe", 32 | "cosi", 33 | "param", 34 | "icon" 35 | ], 36 | [ 37 | "7z.exe", 38 | "empty", 39 | "", 40 | "" 41 | ], 42 | [ 43 | "singtwo" 44 | ] 45 | ], 46 | "checkver": "Download 7-Zip ([\\d.]+)", 47 | "autoupdate": { 48 | "architecture": { 49 | "64bit": { 50 | "url": "https://7-zip.org/a/7z$cleanVersion-x64.msi" 51 | }, "32bit": { 52 | "url": "https://7-zip.org/a/7z$cleanVersion.msi" 53 | } 54 | } 55 | }, 56 | "shortcuts": [ 57 | [ "7zFM.exe", 58 | "7-Zip" 59 | ], ["name with spaces.exe", "Shortcut with spaces in name"] ]} 60 | -------------------------------------------------------------------------------- /test/fixtures/format/unformatted/4-script-block.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.6", 3 | "description": "Rambox Pro. Free, Open Source and Cross Platform messaging and emailing app that combines common web applications into one.", 4 | "homepage": "https://rambox.pro/", 5 | "url": "https://github.com/ramboxapp/download/releases/download/v1.0.6/RamboxPro-1.0.6-win.exe#/cosi.7z", 6 | "hash": "sha512:f4a1b5e12ae15c9a1339fef56b0522b6619d6c23b0ab806f128841c2ba7ce9d9c997fea81f5bc4a24988aed672a4415ff353542535dc7869b5e496f2f1e1efff", 7 | "extract_dir": "\\$PLUGINSDIR", 8 | "pre_install": ["Get-ChildItem \"$dir\" -Exclude 'app-64.7z', 'app-32.7z' | Remove-Item -Force -Recurse"], 9 | "architecture": { 10 | "64bit": { 11 | "installer": { 12 | "script": [ 13 | "Expand-7zipArchive \"$dir\\app-64.7z\" \"$dir\"" 14 | ] 15 | } 16 | }, 17 | "32bit": { 18 | "installer": { 19 | "script": [ 20 | "Expand-7zipArchive \"$dir\\app-32.7z\" \"$dir\"" 21 | ] 22 | } 23 | } 24 | }, 25 | "post_install": ["Remove-Item \"$dir\\app-64.7z\", \"$dir\\app-32.7z\""], 26 | "shortcuts": [ 27 | [ 28 | "RamboxPro.exe", 29 | "RamboxPro" 30 | ] 31 | ], 32 | "checkver": { 33 | "github": "https://github.com/ramboxapp/download/" 34 | }, 35 | "autoupdate": { 36 | "url": "https://github.com/ramboxapp/download/releases/download/v$version/RamboxPro-$version-win.exe#/cosi.7z", 37 | "hash": { 38 | "url": "https://github.com/ramboxapp/download/releases/download/v$version/latest.yml", 39 | "find": "sha512:\\s+(.*)" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/fixtures/is_directory/i_am_a_directory/.gitkeep: -------------------------------------------------------------------------------- 1 | need some content to not fail tests 2 | -------------------------------------------------------------------------------- /test/fixtures/is_directory/i_am_a_file.txt: -------------------------------------------------------------------------------- 1 | dummy content 2 | -------------------------------------------------------------------------------- /test/fixtures/manifest/broken_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://scoop.sh/draft/schema#", 3 | "$schema": "http://scoop.sh/draft/schema#", 4 | "title": "scoop app manifest schema", 5 | "type": "object" 6 | "properties": { 7 | "version": { 8 | "type": "string" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/manifest/broken_wget.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://eternallybored.org/misc/wget/", 3 | "license": "GPL3", 4 | "version": "1.16.3" 5 | "architecture": { 6 | "64bit": { 7 | "url": [ 8 | "https://eternallybored.org/misc/wget/wget-1.16.3-win64.zip", 9 | "http://curl.haxx.se/ca/cacert.pem" 10 | ], 11 | "hash": [ 12 | "85e5393ffd473f7bec40b57637fd09b6808df86c06f846b6885b261a8acac8c5", 13 | "" 14 | ] 15 | }, 16 | "32bit": { 17 | "url": [ 18 | "https://eternallybored.org/misc/wget/wget-1.16.3-win32.zip", 19 | "http://curl.haxx.se/ca/cacert.pem" 20 | ], 21 | "hash": [ 22 | "2ef82af3070abfdaf3862baff0bffdcb3c91c8d75e2f02c8720d90adb9d7a8f7", 23 | "" 24 | ] 25 | } 26 | }, 27 | "bin": "wget.exe", 28 | "post_install": "\"ca_certificate=$dir\\cacert.pem\" | out-file $dir\\wget.ini -encoding default" 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/manifest/invalid_wget.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://eternallybored.org/misc/wget/", 3 | "randomproperty": "should fail", 4 | "license": "GPL3", 5 | "architecture": { 6 | "64bit": { 7 | "url": [ 8 | "https://eternallybored.org/misc/wget/wget-$version-win64.zip", 9 | "http://curl.haxx.se/ca/cacert.pem" 10 | ], 11 | "hash": [ 12 | "85e5393ffd473f7bec40b57637fd09b6808df86c06f846b6885b261a8acac8c5", 13 | "" 14 | ] 15 | }, 16 | "32bit": { 17 | "url": [ 18 | "https://eternallybored.org/misc/wget/wget-$version-win32.zip", 19 | "http://curl.haxx.se/ca/cacert.pem" 20 | ], 21 | "hash": [ 22 | "2ef82af3070abfdaf3862baff0bffdcb3c91c8d75e2f02c8720d90adb9d7a8f7", 23 | "" 24 | ] 25 | } 26 | }, 27 | "bin": "wget.exe", 28 | "post_install": "\"ca_certificate=$dir\\cacert.pem\" | out-file $dir\\wget.ini -encoding default" 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/manifest/wget.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://eternallybored.org/misc/wget/", 3 | "license": "GPL3", 4 | "version": "1.16.3", 5 | "architecture": { 6 | "64bit": { 7 | "url": [ 8 | "https://eternallybored.org/misc/wget/wget-1.16.3-win64.zip", 9 | "http://curl.haxx.se/ca/cacert.pem" 10 | ], 11 | "hash": [ 12 | "85e5393ffd473f7bec40b57637fd09b6808df86c06f846b6885b261a8acac8c5", 13 | "" 14 | ] 15 | }, 16 | "32bit": { 17 | "url": [ 18 | "https://eternallybored.org/misc/wget/wget-1.16.3-win32.zip", 19 | "http://curl.haxx.se/ca/cacert.pem" 20 | ], 21 | "hash": [ 22 | "2ef82af3070abfdaf3862baff0bffdcb3c91c8d75e2f02c8720d90adb9d7a8f7", 23 | "" 24 | ] 25 | } 26 | }, 27 | "bin": "wget.exe", 28 | "post_install": "\"ca_certificate=$dir\\cacert.pem\" | out-file $dir\\wget.ini -encoding default" 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user with 'quote/_tmp/subdir/test.txt: -------------------------------------------------------------------------------- 1 | this is the one 2 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user with 'quote/_tmp/test.txt: -------------------------------------------------------------------------------- 1 | testing 2 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user with space/_tmp/subdir/test.txt: -------------------------------------------------------------------------------- 1 | this is the one 2 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user with space/_tmp/test.txt: -------------------------------------------------------------------------------- 1 | testing 2 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user/_tmp/subdir/test.txt: -------------------------------------------------------------------------------- 1 | this is the one 2 | -------------------------------------------------------------------------------- /test/fixtures/movedir/user/_tmp/test.txt: -------------------------------------------------------------------------------- 1 | testing 2 | -------------------------------------------------------------------------------- /test/fixtures/shim/shim-test.ps1: -------------------------------------------------------------------------------- 1 | 'Hello, world!' 2 | -------------------------------------------------------------------------------- /test/fixtures/shim/user with 'quote/shim-test.ps1: -------------------------------------------------------------------------------- 1 | 'Hello, world!' 2 | --------------------------------------------------------------------------------