112 Commits

Author SHA1 Message Date
Hsiao-nan Cheung
5d46f67e34 Merge branch 'develop' into feature/install_via_git_history 2025-09-26 23:51:08 +08:00
abgox
4c55e7aebd feat(scoop-uninstall): Allow access to $bucket in uninstall scripts (#6380) 2025-09-26 17:14:09 +08:00
Hsiao-nan Cheung
db8d554f80 fix(scoop-update): Force sync tags w/ remote branch while scoop update (#6439) 2025-09-26 15:20:33 +08:00
z-Fng
b592b38abd feat(install): Add separator at the end of notes, highlight suggestions (#6418) 2025-09-24 16:52:03 +08:00
z-Fng
25d35007c6 fix(core|manifest): Avoid error when searching non-existent 'deprecated' directory (#6471) 2025-09-24 14:45:01 +08:00
Bill ZHANG
1fae80b2c2 test: enhance Find-HistoricalManifestInCache with DataTable mock for improved manifest retrieval 2025-08-27 23:20:19 +10:00
Bill ZHANG
cf345e272e test: add behavior-oriented mocks for Invoke-Git to simulate HEAD version retrieval 2025-08-27 22:59:48 +10:00
Bill ZHANG
99fb8383b7 fix(manifest): enforce DataTable contract at caller; select latest matching version via h'^/h with validated fallback 2025-08-27 22:39:02 +10:00
Bill ZHANG
0a52f0d0de refactor(manifest): simplify history lookup and error handling; update tests 2025-08-27 20:45:54 +10:00
z-Fng
04b7ce79c7 fix(scoop-version): Fix logic error caused by missing brackets (#6463) 2025-08-27 17:46:16 +08:00
Bill ZHANG
becd429581 refactor(manifest): Streamline historical manifest retrieval and add Write-ManifestToUserCache helper function 2025-08-27 19:05:22 +10:00
Bill ZHANG
d3f681ce03 refactor(manifest): Simplify UTF-8 encoding handling and enhance test coverage 2025-08-27 18:19:56 +10:00
z-Fng
ff38652e01 fix(buckets|scoop-info): Switch git log date format to ISO 8601 to avoid locale issues (#6446) 2025-08-21 14:18:33 +08:00
HUMORCE
c9f0728ff7 fix(autoupdate): Use origin url to handle urls with fragment in github mode (#6455) 2025-08-20 13:59:47 +08:00
AkariiinMKII
691c23330c fix(scoop-download): Fix function nightly_version not defined error (#6386) 2025-08-14 17:51:19 +08:00
Hsiao-nan Cheung
9f62e55862 Merge branch 'develop' into feature/install_via_git_history 2025-08-12 18:29:52 +08:00
Hsiao-nan Cheung
b588a06e41 chore(release): Bump to version 0.5.3 (resync) (#6436) 2025-08-12 18:18:31 +08:00
Hsiao-nan Cheung
9246250808 Merge branch 'master' into develop 2025-08-12 15:21:48 +08:00
Hsiao-nan Cheung
8c51a50856 fix(autoupdate): Check hash file before use GitHub SHA256 (#6435) 2025-08-12 14:36:30 +08:00
Hsiao-nan Cheung
ebd8c036fa chore(release): Bump to version 0.5.3 (#6257)
* fix (decompress): `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty (#6092)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* refactor(download): Move download-related functions to 'download.ps1' (#6095)

* fix(download): Fallback to default downloader when aria2 fails (#4292)

* fix(commands): Handling broken aliases (#6141)

* fix(shim): properly check `wslpath`/`cygpath` command first (#6114)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-bucket): Add missing import for `no_junction` envs (#6181)

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>

* docs(chglog): Update to 0.5.3 (#6258)

* perf(shim): Update kiennq-shim to v3.1.2 (#6261)

* fix(decompress): Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config (#6327)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-uninstall): Fix uninstaller does not gain Global state (#6430)

* global arg

* changelog

* refactor(Get-Manifest): Select actual source for manifest (#6142)

* first step

* Revert "first step"

This reverts commit c5907c3e25.

* refactor(Get-Manifest): Select actual source for installed manifest

* rework sub-commands, `scoop-depends` is NOT working at this stage

* URI manifest

* opt

* deprecated manifest

* source of manifests

* source of manifest pt2

- Mark URI(path/URL/UNC/etc.) query as standalone manifest
- Drop `installed` and `available update` items for [query] and [installed] are different sources.

* remove variable preventing I forget it

* scoop-info: fix source of manifest on bucket

* fix `scoop-depends`

* Fix Standalone and Source detection

* fix global install

* Fix scoop-cat, scoop-home

- Query for remote manifest

* scoop-list: info +deprecated

* manifest: Fix first selected manifest

* gramma..

* Fix 61b3259

* length

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false (#6431)

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true

* CHANGELOG

* feat(autoupdate): GitHub predefined hashes support (#6416)

* feat(autoupdate): predefined hash case for GitHub

- Remove `sha256:` prefix in `format_hash()`
- Add GitHub support in `get_hash_for_app()`

Close #6381

* doc(chglog): GitHub auto hash update

* fix(autoupdate): remove prefix only

* docs(CHANGELOG): Update to 0.5.3 (#6432)

* docs(CHANGELOG): Update to 0.5.3

* 6416

---------

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Olav Rønnestad Birkeland <6450056+o-l-a-v@users.noreply.github.com>
Co-authored-by: kiennq <kien.n.quang@gmail.com>
Co-authored-by: HUMORCE <humorce@outlook.com>
Co-authored-by: Ryan <sitiom@proton.me>
Co-authored-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Bassel Rachid <101208715+basselworkforce@users.noreply.github.com>
Co-authored-by: Wordless Echo <wordless@echo.moe>
2025-08-11 00:38:38 +00:00
HUMORCE
ed6101a2ab docs(CHANGELOG): Update to 0.5.3 (#6432)
* docs(CHANGELOG): Update to 0.5.3

* 6416
2025-08-11 00:34:50 +00:00
Wordless Echo
26a03e2404 feat(autoupdate): GitHub predefined hashes support (#6416)
* feat(autoupdate): predefined hash case for GitHub

- Remove `sha256:` prefix in `format_hash()`
- Add GitHub support in `get_hash_for_app()`

Close #6381

* doc(chglog): GitHub auto hash update

* fix(autoupdate): remove prefix only
2025-08-11 00:11:46 +00:00
HUMORCE
f9c4f9e175 fix(scoop-depends-tests): Mocking USE_EXTERNAL_7ZIP as $false (#6431)
* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true

* CHANGELOG
2025-08-10 23:49:22 +00:00
HUMORCE
3321b969a4 refactor(Get-Manifest): Select actual source for manifest (#6142)
* first step

* Revert "first step"

This reverts commit c5907c3e25.

* refactor(Get-Manifest): Select actual source for installed manifest

* rework sub-commands, `scoop-depends` is NOT working at this stage

* URI manifest

* opt

* deprecated manifest

* source of manifests

* source of manifest pt2

- Mark URI(path/URL/UNC/etc.) query as standalone manifest
- Drop `installed` and `available update` items for [query] and [installed] are different sources.

* remove variable preventing I forget it

* scoop-info: fix source of manifest on bucket

* fix `scoop-depends`

* Fix Standalone and Source detection

* fix global install

* Fix scoop-cat, scoop-home

- Query for remote manifest

* scoop-list: info +deprecated

* manifest: Fix first selected manifest

* gramma..

* Fix 61b3259

* length
2025-08-10 23:04:34 +00:00
HUMORCE
22f1672fcb fix(scoop-uninstall): Fix uninstaller does not gain Global state (#6430)
* global arg

* changelog
2025-08-10 22:48:06 +00:00
Bill ZHANG
887f79d2d7 refactor: remove new tests 2025-07-21 13:33:16 +10:00
Bill ZHANG
5bae77eebb refactor: Enhance Select-ScoopDBItem and Get-ScoopDBItem functions for improved data handling and output 2025-07-21 12:48:41 +10:00
Bill ZHANG
210b5e45a0 fix: Improve user guidance and error handling in generate_user_manifest function 2025-07-21 12:26:31 +10:00
Bill ZHANG
3aa64baeb1 refactor: Remove deprecated install_app tests and enhance Get-HistoricalManifestFromDB for exact version matching 2025-07-21 10:56:38 +10:00
Bill ZHANG
0470ba2d04 refactor: Enhance historical manifest retrieval with improved user guidance and fallback mechanisms 2025-07-21 10:47:07 +10:00
Bill ZHANG
398aaa2330 fix: Update installation instructions to clarify search order for app versions 2025-07-21 10:44:12 +10:00
Bill ZHANG
4585f50a54 Remove best match logic from Get-HistoricalManifestFromDB
- Keep only exact match logic (lines 177-189)
- Return null immediately when no exact match found (line 193)
- Remove trailing whitespace from manifest.ps1
- Function now only supports exact version matches, no compatibility matching
2025-07-21 10:29:54 +10:00
Bill ZHANG
9a6cdc7328 Fix test failures: improve parameter validation, file formatting, and test expectations
- Add missing psmodules.ps1 import to Install tests
- Fix architecture parameter validation (x64 -> 64bit)
- Add null/empty bucket parameter handling in Get-HistoricalManifestFromDB
- Fix file format issues (add missing newlines, remove trailing whitespace)
- Update JSON parsing tests to match actual implementation behavior
- Fix path separator normalization in Get-RelativePathCompat test
- Remove invalid error handling tests for non-existent try-catch blocks
- Reduce test failures from 11 to 4, with remaining issues being mock-related

All fixes respect existing codebase behavior and design decisions.
2025-07-20 23:26:59 +10:00
Bill ZHANG
039d2d77c2 revert: remove out-of-scope error handling changes from install.ps1
The try-catch block addition was a general error handling improvement
that belongs in a separate PR, not in this git history feature PR.
2025-07-20 23:10:14 +10:00
Bill ZHANG
ec44c6c7b2 refactor: separate git history concerns from installation layer
- Remove git history fallback logic from install_app function
- Move git history concerns to manifest layer where they belong
- Improve separation of concerns and reduce coupling
- Update tests to reflect the cleaner design
- Merge git history tests into Scoop-Install.Tests.ps1 for consistency

This follows the 'Pull complexity downward' principle by keeping
installation logic focused on installation, while manifest resolution
handles all version fallback strategies internally.
2025-07-20 22:54:33 +10:00
Bill ZHANG
c264afc6bd fix: Handle git command array output for all PowerShell versions
- Fix git show command returning arrays instead of strings
- Join array elements with newlines to create proper JSON content
- Remove misleading PowerShell 5 specific comments
- Fixes historical manifest search in both PowerShell 5 and 7
2025-07-20 21:49:44 +10:00
Bill ZHANG
4061bff270 fix: Add PowerShell 5 compatibility for UTF8 encoding
- Use Out-UTF8File instead of Set-Content with UTF8Encoding for PowerShell 5
- Add version checks to use appropriate encoding methods
- Fixes 'Cannot bind parameter Encoding' error in Windows PowerShell 5
- Ensures historical manifest search works on all PowerShell versions
2025-07-20 21:32:58 +10:00
Bill ZHANG
3e3ce096b8 docs: Fix comment mismatch for python version example
- Change '3.7.x' to '3.12.x' to match the command 'python@3.12'
- Addresses GitHub Copilot's documentation consistency feedback
2025-07-20 21:27:47 +10:00
Bill ZHANG
83696854a8 fix: Remove trailing whitespaces from manifest.ps1
- Fix code style issues to pass tests
- Maintain existing functionality without changes
2025-07-20 21:09:55 +10:00
Bill ZHANG
82a35323d3 feat: Prioritize SQLite database for historical manifest search
- Add Get-HistoricalManifestFromDB function to search SQLite cache first
- Modify Get-HistoricalManifest to use SQLite before falling back to git history
- Implement best version matching for SQLite database results
- Add conditional database import to ensure functions are available
- Improve user messages to indicate whether cache or git history is used

This addresses @niheaven's suggestion to use SQLite for faster historical version searches, significantly reducing performance bottlenecks when searching for approximate versions.
2025-07-20 21:02:16 +10:00
Bill ZHANG
dfadfcc1dc style: Remove extra blank line from CHANGELOG.md 2025-07-20 20:54:16 +10:00
Bill ZHANG
0ade4a5344 fix: Add Windows PowerShell compatibility for relative path operations
- Add Get-RelativePathCompat function that falls back to Uri-based implementation for PowerShell 5.x
- Replace direct GetRelativePath call with compatible wrapper
- Ensures historical manifest feature works across all PowerShell versions
2025-07-20 20:53:40 +10:00
Bill ZHANG
ce95819fa5 docs(changelog): update changelog 2025-05-26 22:03:23 +10:00
Bill ZHANG
c7795b44af feat(install): Add historical manifest search for app installation
- Implemented try-catch block in `install_app` function to handle installation failures and provide informative error messages.
- Added `Get-HistoricalManifest` function to search for specific app versions in git history, enhancing version management during installation.
- Updated `generate_user_manifest` to utilize historical manifests as a fallback when autoupdate fails.
- Enhanced documentation in `scoop-config.ps1` to clarify the use of git history for version installation.
2025-05-26 20:48:27 +10:00
Bassel Rachid
4e528e1655 fix(decompress): Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config (#6327)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2025-04-17 11:19:22 +08:00
Hsiao-nan Cheung
ac6f1baaf4 perf(shim): Update kiennq-shim to v3.1.2 (#6261) 2025-01-03 14:34:38 +08:00
Hsiao-nan Cheung
dc9d198014 docs(chglog): Update to 0.5.3 (#6258) 2024-12-30 13:55:32 +08:00
Chawye Hsu
84e00fdb77 fix(scoop-bucket): Add missing import for no_junction envs (#6181)
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
2024-10-11 14:20:43 +08:00
Ryan
7a309a1b00 fix(shim): properly check wslpath/cygpath command first (#6114)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-10-09 19:10:42 +08:00
HUMORCE
3577f91d82 fix(commands): Handling broken aliases (#6141) 2024-09-20 14:18:13 +08:00
kiennq
e0c682de7c fix(download): Fallback to default downloader when aria2 fails (#4292) 2024-08-26 16:10:30 +08:00
Hsiao-nan Cheung
79cf33d0b7 refactor(download): Move download-related functions to 'download.ps1' (#6095) 2024-08-11 17:23:59 +08:00
Olav Rønnestad Birkeland
7f99c499d7 fix (decompress): Expand-7zipArchive only delete temp dir / $extractDir if it is empty (#6092)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-08-09 11:03:51 +08:00
Hsiao-nan Cheung
859d1db51b chore(release): Bump to version 0.5.2 (#6080)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
Co-authored-by: Matt Cargile <74641867+mattcargile@users.noreply.github.com>
Co-authored-by: ccyykkcyk <91893497+ccyykkcyk@users.noreply.github.com>
2024-07-30 15:34:41 +08:00
Hsiao-nan Cheung
c7ec5c82b2 docs(chglog): Update CHANGELOG for v0.5.2 (#6081) 2024-07-25 20:29:02 +08:00
Hsiao-nan Cheung
46624b00c9 fix(core): Use 'Join-Path' to construct cache file path (#6079) 2024-07-25 13:54:26 +08:00
Hsiao-nan Cheung
49ee8ad6ec fix(scoop-hold): Use 'foreach' loop to allow 'continue' statement (#6078) 2024-07-25 13:00:36 +08:00
Hsiao-nan Cheung
5dc5dd22f8 fix(json): Fix JSON items counting function (#6073) 2024-07-23 19:55:40 +08:00
Hsiao-nan Cheung
5971f1dda8 builds(supporting): Update Json.Schema to 4.0.1 (#6072) 2024-07-23 19:13:00 +08:00
ccyykkcyk
0cb479abf0 fix(json): Don't serialize jsonpath return if only one result (#6066)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-07-22 19:02:06 +08:00
Matt Cargile
429ba7af5b fix(scoop-alias): Fix 'Option --verbose not recognized.' (#6062) 2024-07-17 22:12:29 +08:00
Hsiao-nan Cheung
be56faf290 chore(release): Bump to version 0.5.1 (#6057) 2024-07-17 10:38:53 +08:00
Hsiao-nan Cheung
69edc6dc54 chore(chglog): Update CHANGELOG for v0.5.1 (#6058) 2024-07-15 13:17:19 +08:00
L. Yeung
0138dc4266 fix(scoop-alias): Pass options correctly (#6003)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-07-15 13:06:22 +08:00
Bill ZHANG
1c271f5b90 fix(scoop-virustotal): Adjust json_path parameters to retrieve correct analysis stats (#6044) 2024-07-09 13:45:09 +08:00
qiuyk
a76884af19 fix(install): Fix parsing error when installing multiple apps w/ specific version (#6039)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-07-09 13:22:06 +08:00
Hsiao-nan Cheung
7cc4faea1d fix(bucket): Implement error handling for failed bucket addition (#6051) 2024-07-09 01:16:23 +08:00
Hsiao-nan Cheung
83f25a4673 fix(install): Expand env_set items (#6050) 2024-07-09 00:24:42 +08:00
Hsiao-nan Cheung
42af27d16c fix(sqlite): Fix compatibility with Windows PowerShell (#6045) 2024-07-08 13:43:22 +08:00
Hsiao-nan Cheung
716b6db3ae chore(release): Bump to version 0.5.0 (#6035) 2024-07-05 11:14:42 +08:00
Hsiao-nan Cheung
ade7aa4a15 (chore): Update changelog (#6036) 2024-06-30 18:19:19 +08:00
Hsiao-nan Cheung
7e0a2a28c6 Update chglog 2024-06-30 18:09:59 +08:00
Hsiao-nan Cheung
2d02483fb8 Merge branch 'master' into develop 2024-06-30 18:07:36 +08:00
Hsiao-nan Cheung
93359a43a1 fix(shim): Restore original path for JAR cmd (#6030) 2024-06-26 18:34:38 +08:00
L. Yeung
d8b3cc8a6c fix(checkver): Correct error messages (#6024) 2024-06-25 10:40:56 +08:00
Hsiao-nan Cheung
9239c26bbd feat(decompress): Use innounp-unicode as default Inno Setup Unpacker (#6028) 2024-06-24 21:20:29 +08:00
qiuyk
3a39ba048f fix(core): Limit the number of commands to get (#6013) 2024-06-14 00:18:14 +08:00
Hsiao-nan Cheung
a9ca75c0cd fix(core): Use correct path in 'bash' (#6006) 2024-06-11 11:03:35 +08:00
Matej Kafka
d20819e147 fix(core): Search for Git executable instead of any cmdlet (#5998) 2024-06-07 18:45:52 +08:00
L. Yeung
dec4232754 fix(decompress): Match extract_dir/extract_to and archives (#5983) 2024-05-26 15:36:06 +08:00
Chawye Hsu
fa1b42bf28 fix(checkver): Correct variable 'regex' to 'regexp' (#5993) 2024-05-26 14:30:47 +08:00
insertokername
700a2f4f5e feat(install): Added the ability to specify @ version when installing from url/file location (#5988) 2024-05-23 23:05:38 +08:00
Hsiao-nan Cheung
c9048ad978 fix(sqlite): Fix generic class error in PS5 (#5981) 2024-05-20 20:57:29 +08:00
Hsiao-nan Cheung
5c896e901f refactor(decompress): Use 7zip to extract Zstd archive (#5973) 2024-05-17 09:56:14 +08:00
Hsiao-nan Cheung
8ea37387ae fix(install): Add back arg $Name in Invoke-Installer() (#5971) 2024-05-15 19:25:35 +08:00
Hsiao-nan Cheung
2544745695 refactor(install): Replace 'run_(un)installer()' with 'Invoke-Installer()' (#5968) 2024-05-15 19:03:54 +08:00
Hsiao-nan Cheung
5ce70c4139 fix(sqlite): Update cache after removing bucket or manifests (#5967) 2024-05-15 17:07:37 +08:00
L. Yeung
2d50a02a32 fix(scoop-download|install|update): Use consistent options (#5956) 2024-05-14 22:43:53 +08:00
Hsiao-nan Cheung
f6f46f6cf4 fix(sqlite): Dispose all command to release database file handle (#5966) 2024-05-14 16:38:05 +08:00
Hsiao-nan Cheung
d337bb1fa6 chore(release): Bump to version 0.4.2 (#5964)
Co-authored-by: Xuesong <amorphobia@users.noreply.github.com>
Co-authored-by: Inseo Lee <capella87@outlook.com>
2024-05-14 13:44:20 +08:00
Hsiao-nan Cheung
cddcd98e33 doc(chglog): Update CHANGLOG 2024-05-13 19:02:43 +08:00
Hsiao-nan Cheung
86d3fef187 fix(core): Add 'PSObject.Copy()' to 'substitute()' (#5962) 2024-05-13 18:53:30 +08:00
Hsiao-nan Cheung
e324fa4c5f fix(core): Fix "Invoke-ExternalCommand" quoting rules (#5945) 2024-05-13 18:53:15 +08:00
Inseo Lee
d9c5c90285 fix(system): Fix argument passing to Split-PathLikeEnvVar() in deprecated strip_path() (#5937)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-05-13 18:53:15 +08:00
Xuesong
82640c456e fix(autoupdate): Copy PSCustomObject-type properties within autoupdate to prevent reference changes (#5934) 2024-05-13 18:53:15 +08:00
Hsiao-nan Cheung
4dd2cfdc5f fix(core): Add 'PSObject.Copy()' to 'substitute()' (#5962) 2024-05-13 13:46:59 +08:00
Hsiao-nan Cheung
a5bd2297c6 refactor(install): Separate archive extraction from downloader (#5951) 2024-05-12 18:47:16 +08:00
Hsiao-nan Cheung
b710ff6c0a fix(scoop-info): Fix download size estimating (#5958) 2024-05-11 18:22:31 +08:00
Hsiao-nan Cheung
1dd479f0be fix(database): Use 'Find-BucketDirectory()' to locate bucket dir (#5955) 2024-05-11 14:50:20 +08:00
Hsiao-nan Cheung
1752050614 fix(core): Fix "Invoke-ExternalCommand" quoting rules (#5945) 2024-05-11 14:49:49 +08:00
Hsiao-nan Cheung
edaae8d39f fix(database): Skip caching 'deprecated' dir (#5949) 2024-05-09 20:15:57 +08:00
Hsiao-nan Cheung
776135e0ab fix(database): Update cache when adding bucket (#5946) 2024-05-08 18:10:13 +08:00
Chawye Hsu
cc863353e2 fix(scoop-cache): Fix regression in 36026f18 (#5944)
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
2024-05-08 11:24:41 +08:00
Inseo Lee
5b86c302e5 fix(system): Fix argument passing to Split-PathLikeEnvVar() in deprecated strip_path() (#5937)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-05-06 10:04:40 +08:00
Xuesong
b8580aa931 fix(autoupdate): Copy PSCustomObject-type properties within autoupdate to prevent reference changes (#5934) 2024-05-01 23:02:13 +08:00
Chawye Hsu
23d55ced72 fix(scoop-search): Catch error of parsing invalid manifest (#5930) 2024-05-01 00:52:49 +08:00
Chawye Hsu
36026f1804 feat(core): New cache filename format (#5929)
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-04-29 18:36:19 +08:00
Chawye Hsu
3b34497eb4 fix(json): Serialize jsonpath return (#5921) 2024-04-26 18:12:34 +08:00
Hsiao-nan Cheung
bb88dfb2d4 Sync 'master' branch 2024-04-25 22:09:16 +08:00
Chawye Hsu
d285bb08d4 chore(release): Bump to version 0.4.1 (#5924)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-04-25 21:33:45 +08:00
Hsiao-nan Cheung
eac58400db fix(sqlite): Skip use_sqlite_cache config on ARM64 platform (#5918) 2024-04-25 21:32:41 +08:00
Hsiao-nan Cheung
2dd91d5ba3 feat(sqlite): Use SQLite for caching apps to speed up local search (#5851) 2024-04-19 14:02:50 +08:00
58 changed files with 2634 additions and 1416 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ test/installer/tmp/*
test/tmp/*
*~
TestResults.xml
supporting/sqlite/*

View File

@@ -1,3 +1,113 @@
## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/v0.5.3...develop)
### Features
- **install:** Add support for historical manifests ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370))
- **scoop-uninstall**: Allow access to `$bucket` in uninstall scripts ([#6380](https://github.com/ScoopInstaller/Scoop/issues/6380))
- **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418))
### Bug Fixes
- **scoop-update**: Force sync tags w/ remote branch while scoop update ([#6439](https://github.com/ScoopInstaller/Scoop/issues/6439))
- **scoop-download:** Fix function `nightly_version` not defined error ([#6386](https://github.com/ScoopInstaller/Scoop/issues/6386))
- **autoupdate:** Use origin URL to handle URLs with fragment in GitHub mode ([#6455](https://github.com/ScoopInstaller/Scoop/issues/6455))
- **buckets|scoop-info:** Switch git log date format to ISO 8601 to avoid locale issues ([#6446](https://github.com/ScoopInstaller/Scoop/issues/6446))
- **scoop-version:** Fix logic error caused by missing brackets ([#6463](https://github.com/ScoopInstaller/Scoop/issues/6463))
- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11
### Features
- **autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
### Bug Fixes
- **scoop-download|install|update:** Fallback to default downloader when aria2 fails ([#4292](https://github.com/ScoopInstaller/Scoop/issues/4292))
- **decompress**: `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
- **decompress**: Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config with USE_EXTERNAL_7ZIP ([#6327](https://github.com/ScoopInstaller/Scoop/issues/6327))
- **commands**: Handling broken aliases ([#6141](https://github.com/ScoopInstaller/Scoop/issues/6141))
- **shim:** Do not suppress `stderr`, properly check `wslpath`/`cygpath` command first ([#6114](https://github.com/ScoopInstaller/Scoop/issues/6114))
- **scoop-bucket:** Add missing import for `no_junction` envs ([#6181](https://github.com/ScoopInstaller/Scoop/issues/6181))
- **scoop-uninstall:** Fix uninstaller does not gain Global state ([#6430](https://github.com/ScoopInstaller/Scoop/issues/6430))
- **scoop-depends-tests:** Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true ([#6431](https://github.com/ScoopInstaller/Scoop/issues/6431))
### Code Refactoring
- **download:** Move download-related functions to 'download.ps1' ([#6095](https://github.com/ScoopInstaller/Scoop/issues/6095))
- **Get-Manifest:** Select actual source for manifest ([#6142](https://github.com/ScoopInstaller/Scoop/issues/6142))
### Performance Improvements
- **shim:** Update kiennq-shim to v3.1.2 ([#6261](https://github.com/ScoopInstaller/Scoop/issues/6261))
## [v0.5.2](https://github.com/ScoopInstaller/Scoop/compare/v0.5.1...v0.5.2) - 2024-07-26
### Bug Fixes
- **scoop-alias:** Fix 'Option --verbose not recognized.' ([#6062](https://github.com/ScoopInstaller/Scoop/issues/6062))
- **scoop-hold:** Use 'foreach' loop to allow 'continue' statement ([#6078](https://github.com/ScoopInstaller/Scoop/issues/6078))
- **core:** Use 'Join-Path' to construct cache file path ([#6079](https://github.com/ScoopInstaller/Scoop/issues/6079))
- **json:** Don't serialize jsonpath return if only one result ([#6066](https://github.com/ScoopInstaller/Scoop/issues/6066), [#6073](https://github.com/ScoopInstaller/Scoop/issues/6073))
### Builds
- **supporting:** Update Json.Schema to 4.0.1 ([#6072](https://github.com/ScoopInstaller/Scoop/issues/6072))
## [v0.5.1](https://github.com/ScoopInstaller/Scoop/compare/v0.5.0...v0.5.1) - 2024-07-16
### Bug Fixes
- **scoop-alias:** Pass options correctly ([#6003](https://github.com/ScoopInstaller/Scoop/issues/6003))
- **scoop-virustotal:** Adjust `json_path` parameters to retrieve correct analysis stats ([#6044](https://github.com/ScoopInstaller/Scoop/issues/6044))
- **bucket:** Implement error handling for failed bucket addition ([#6051](https://github.com/ScoopInstaller/Scoop/issues/6051))
- **database:** Fix compatibility with Windows PowerShell ([#6045](https://github.com/ScoopInstaller/Scoop/issues/6045))
- **install:** Expand `env_set` items before setting Environment Variables ([#6050](https://github.com/ScoopInstaller/Scoop/issues/6050))
- **install:** Fix parsing error when installing multiple apps w/ specific version ([#6039](https://github.com/ScoopInstaller/Scoop/issues/6039))
## [v0.5.0](https://github.com/ScoopInstaller/Scoop/compare/v0.4.2...v0.5.0) - 2024-07-01
### Features
- **scoop-search:** Use SQLite for caching apps to speed up local search ([#5851](https://github.com/ScoopInstaller/Scoop/issues/5851), [#5918](https://github.com/ScoopInstaller/Scoop/issues/5918), [#5946](https://github.com/ScoopInstaller/Scoop/issues/5946), [#5949](https://github.com/ScoopInstaller/Scoop/issues/5949), [#5955](https://github.com/ScoopInstaller/Scoop/issues/5955), [#5966](https://github.com/ScoopInstaller/Scoop/issues/5966), [#5967](https://github.com/ScoopInstaller/Scoop/issues/5967), [#5981](https://github.com/ScoopInstaller/Scoop/issues/5981))
- **core:** New cache filename format ([#5929](https://github.com/ScoopInstaller/Scoop/issues/5929), [#5944](https://github.com/ScoopInstaller/Scoop/issues/5944))
- **decompress:** Use innounp-unicode as default Inno Setup Unpacker ([#6028](https://github.com/ScoopInstaller/Scoop/issues/6028))
- **install:** Added the ability to install specific version of app from URL/file link ([#5988](https://github.com/ScoopInstaller/Scoop/issues/5988))
### Bug Fixes
- **scoop-download|install|update:** Use consistent options ([#5956](https://github.com/ScoopInstaller/Scoop/issues/5956))
- **scoop-info:** Fix download size estimating ([#5958](https://github.com/ScoopInstaller/Scoop/issues/5958))
- **scoop-search:** Catch error of parsing invalid manifest ([#5930](https://github.com/ScoopInstaller/Scoop/issues/5930))
- **checkver:** Correct variable 'regex' to 'regexp' ([#5993](https://github.com/ScoopInstaller/Scoop/issues/5993))
- **checkver:** Correct error messages ([#6024](https://github.com/ScoopInstaller/Scoop/issues/6024))
- **core:** Search for Git executable instead of any cmdlet ([#5998](https://github.com/ScoopInstaller/Scoop/issues/5998))
- **core:** Use correct path in 'bash' ([#6006](https://github.com/ScoopInstaller/Scoop/issues/6006))
- **core:** Limit the number of commands to get when search for git executable ([#6013](https://github.com/ScoopInstaller/Scoop/issues/6013))
- **decompress:** Match `extract_dir`/`extract_to` and archives ([#5983](https://github.com/ScoopInstaller/Scoop/issues/5983))
- **json:** Serialize jsonpath return ([#5921](https://github.com/ScoopInstaller/Scoop/issues/5921))
- **shim:** Restore original path for JAR cmd ([#6030](https://github.com/ScoopInstaller/Scoop/issues/6030))
### Code Refactoring
- **decompress:** Use 7zip to extract Zstd archive ([#5973](https://github.com/ScoopInstaller/Scoop/issues/5973))
- **install:** Separate archive extraction from downloader ([#5951](https://github.com/ScoopInstaller/Scoop/issues/5951))
- **install:** Replace 'run_(un)installer()' with 'Invoke-Installer()' ([#5968](https://github.com/ScoopInstaller/Scoop/issues/5968), [#5971](https://github.com/ScoopInstaller/Scoop/issues/5971))
## [v0.4.2](https://github.com/ScoopInstaller/Scoop/compare/v0.4.1...v0.4.2) - 2024-05-14
### Bug Fixes
- **autoupdate:** Copy `PSCustomObject`-type properties within `substitute()` to prevent reference changes ([#5934](https://github.com/ScoopInstaller/Scoop/issues/5934), [#5962](https://github.com/ScoopInstaller/Scoop/issues/5962))
- **core:** Fix `Invoke-ExternalCommand` quoting rules ([#5945](https://github.com/ScoopInstaller/Scoop/issues/5945))
- **system:** Fix argument passing to `Split-PathLikeEnvVar()` in deprecated `strip_path()` ([#5937](https://github.com/ScoopInstaller/Scoop/issues/5937))
## [v0.4.1](https://github.com/ScoopInstaller/Scoop/compare/v0.4.0...v0.4.1) - 2024-04-25
### Bug Fixes
- **core:** Fix `Invoke-ExternalCommand` regression ([#5923](https://github.com/ScoopInstaller/Scoop/issues/5923))
## [v0.4.0](https://github.com/ScoopInstaller/Scoop/compare/v0.3.1...v0.4.0) - 2024-04-18
### Features

View File

@@ -46,7 +46,7 @@ param(
. "$PSScriptRoot\..\lib\autoupdate.ps1"
. "$PSScriptRoot\..\lib\json.ps1"
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
if ($ForceUpdate) { $Update = $true }

View File

@@ -28,7 +28,7 @@ param(
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
$Queue = @()

View File

@@ -73,7 +73,7 @@ param(
. "$PSScriptRoot\..\lib\buckets.ps1"
. "$PSScriptRoot\..\lib\json.ps1"
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\install.ps1" # needed for hash generation
. "$PSScriptRoot\..\lib\download.ps1"
if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) {
$Dir = Split-Path $App
@@ -275,7 +275,7 @@ while ($in_progress -gt 0) {
$ver = $Version
if (!$ver) {
if (!$regex -and $replace) {
if (!$regexp -and $replace) {
next "'replace' requires 're' or 'regex'"
continue
}
@@ -294,13 +294,15 @@ while ($in_progress -gt 0) {
}
$page = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd()
}
$source = $url
if ($script) {
$page = Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
$source = 'the output of script'
}
if ($jsonpath) {
# Return only a single value if regex is absent
$noregex = [String]::IsNullOrEmpty($regex)
$noregex = [String]::IsNullOrEmpty($regexp)
# If reverse is ON and regex is ON,
# Then reverse would have no effect because regex handles reverse
# on its own
@@ -310,7 +312,7 @@ while ($in_progress -gt 0) {
$ver = json_path_legacy $page $jsonpath
}
if (!$ver) {
next "couldn't find '$jsonpath' in $url"
next "couldn't find '$jsonpath' in $source"
continue
}
}
@@ -332,7 +334,7 @@ while ($in_progress -gt 0) {
# Getting version from XML, using XPath
$ver = $xml.SelectSingleNode($xpath, $nsmgr).'#text'
if (!$ver) {
next "couldn't find '$($xpath -replace 'ns:', '')' in $url"
next "couldn't find '$($xpath -replace 'ns:', '')' in $source"
continue
}
}
@@ -348,31 +350,31 @@ while ($in_progress -gt 0) {
}
if ($regexp) {
$regex = New-Object System.Text.RegularExpressions.Regex($regexp)
$re = New-Object System.Text.RegularExpressions.Regex($regexp)
if ($reverse) {
$match = $regex.Matches($page) | Select-Object -Last 1
$match = $re.Matches($page) | Select-Object -Last 1
} else {
$match = $regex.Matches($page) | Select-Object -First 1
$match = $re.Matches($page) | Select-Object -First 1
}
if ($match -and $match.Success) {
$matchesHashtable = @{}
$regex.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
$re.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
$ver = $matchesHashtable['1']
if ($replace) {
$ver = $regex.Replace($match.Value, $replace)
$ver = $re.Replace($match.Value, $replace)
}
if (!$ver) {
$ver = $matchesHashtable['version']
}
} else {
next "couldn't match '$regexp' in $url"
next "couldn't match '$regexp' in $source"
continue
}
}
if (!$ver) {
next "couldn't find new version in $url"
next "couldn't find new version in $source"
continue
}
}

View File

@@ -23,6 +23,7 @@ param(
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\description.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
$Queue = @()

View File

@@ -20,8 +20,8 @@ switch ($subCommand) {
}
({ $subCommand -in @('-v', '--version') }) {
Write-Host 'Current Scoop version:'
if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline')
if ((Test-GitAvailable) -and (Test-Path "$PSScriptRoot\..\.git") -and ((get_config SCOOP_BRANCH 'master') -ne 'master')) {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
} else {
$version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md"
Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline
@@ -31,9 +31,9 @@ switch ($subCommand) {
Get-LocalBucket | ForEach-Object {
$bucketLoc = Find-BucketDirectory $_ -Root
if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) {
if ((Test-GitAvailable) -and (Test-Path "$bucketLoc\.git")) {
Write-Host "'$_' bucket:"
Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline')
Invoke-Git -Path $bucketLoc -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
Write-Host ''
}
}

View File

@@ -42,7 +42,7 @@ function do_uninstall($app, $global) {
$architecture = $install.architecture
Write-Output "Uninstalling '$app'"
run_uninstaller $manifest $architecture $dir
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall
rm_shims $app $manifest $global $architecture
# If a junction was used during install, that will have been used

View File

@@ -1,4 +1,24 @@
# Must included with 'json.ps1'
function format_hash([String] $hash) {
$hash = $hash.toLower()
# Workaround for GitHub API:
# `"digest": "sha256:<SHA256_STRING>"`
if ($hash -like 'sha256:*') {
$hash = $hash.Substring(7) # Remove prefix 'sha256:'
}
switch ($hash.Length) {
32 { $hash = "md5:$hash" } # md5
40 { $hash = "sha1:$hash" } # sha1
64 { $hash = $hash } # sha256
128 { $hash = "sha512:$hash" } # sha512
default { $hash = $null }
}
return $hash
}
function find_hash_in_rdf([String] $url, [String] $basename) {
$xml = $null
try {
@@ -191,13 +211,14 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hash = $null
$hashmode = $config.mode
$originurl = strip_fragment $url
$basename = [System.Web.HttpUtility]::UrlDecode((url_remote_filename($url)))
$substitutions = $substitutions.Clone()
$substitutions.Add('$url', (strip_fragment $url))
$substitutions.Add('$baseurl', (strip_filename (strip_fragment $url)).TrimEnd('/'))
$substitutions.Add('$url', $originurl)
$substitutions.Add('$baseurl', (strip_filename $originurl).TrimEnd('/'))
$substitutions.Add('$basename', $basename)
$substitutions.Add('$urlNoExt', (strip_ext (strip_fragment $url)))
$substitutions.Add('$urlNoExt', (strip_ext $originurl))
$substitutions.Add('$basenameNoExt', (strip_ext $basename))
debug $substitutions
@@ -246,6 +267,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hashmode = 'sourceforge'
}
if (!$hashfile_url -and $url -match 'https:\/\/github\.com\/(?<owner>[^\/]+)\/(?<repo>[^\/]+)\/releases\/download\/[^\/]+\/[^\/]+') {
$hashmode = 'github'
}
switch ($hashmode) {
'extract' {
$hash = find_hash_in_textfile $hashfile_url $substitutions $regex
@@ -273,6 +298,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hashfile_url = (strip_filename (strip_fragment "https://sourceforge.net/projects/$($matches['project'])/files/$($matches['file'])")).TrimEnd('/')
$hash = find_hash_in_textfile $hashfile_url $substitutions '"$basename":.*?"sha1":\s*"([a-fA-F0-9]{40})"'
}
'github' {
$hashfile_url = "https://api.github.com/repos/$($matches['owner'])/$($matches['repo'])/releases"
$hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $originurl + "')].digest")
}
}
if ($hash) {

View File

@@ -108,7 +108,7 @@ function list_buckets {
$path = Find-BucketDirectory $_ -Root
if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) {
$bucket.Source = Invoke-Git -Path $path -ArgumentList @('config', 'remote.origin.url')
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aD', '-n', '1') | Get-Date
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aI', '-n', '1') | Get-Date
} else {
$bucket.Source = friendly_path $path
$bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime
@@ -154,8 +154,17 @@ function add_bucket($name, $repo) {
}
ensure $bucketsdir | Out-Null
$dir = ensure $dir
Invoke-Git -ArgumentList @('clone', $repo, $dir, '-q')
$out = Invoke-Git -ArgumentList @('clone', $repo, $dir, '-q')
if ($LASTEXITCODE -ne 0) {
error "Failed to clone '$repo' to '$dir'.`n`nError given:`n$out`n`nPlease check the repository URL or network connection and try again."
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
return 1
}
Write-Host 'OK'
if (get_config USE_SQLITE_CACHE) {
info 'Updating cache...'
Set-ScoopDB -Path (Get-ChildItem (Find-BucketDirectory $name) -Filter '*.json' -Recurse).FullName
}
success "The $name bucket was added successfully."
return 0
}
@@ -168,6 +177,11 @@ function rm_bucket($name) {
}
Remove-Item $dir -Recurse -Force -ErrorAction Stop
if (get_config USE_SQLITE_CACHE) {
info 'Updating cache...'
Remove-ScoopDBItem -Bucket $name
}
success "The $name bucket was removed successfully."
return 0
}

View File

@@ -1,6 +1,10 @@
# Description: Functions for managing commands and aliases.
## Functions for commands
function command_files {
(Get-ChildItem "$PSScriptRoot\..\libexec") + (Get-ChildItem "$scoopdir\shims") |
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
}
function commands {
@@ -19,11 +23,10 @@ function command_path($cmd) {
# get path from shim
$shim_path = "$scoopdir\shims\scoop-$cmd.ps1"
$line = ((Get-Content $shim_path) | Where-Object { $_.startswith('$path') })
if($line) {
if ($line) {
Invoke-Command ([scriptblock]::Create($line)) -NoNewScope
$cmd_path = $path
}
else { $cmd_path = $shim_path }
} else { $cmd_path = $shim_path }
}
$cmd_path
@@ -34,3 +37,92 @@ function exec($cmd, $arguments) {
& $cmd_path @arguments
}
## Functions for aliases
function add_alias {
param(
[ValidateNotNullOrEmpty()]
[string]$name,
[ValidateNotNullOrEmpty()]
[string]$command,
[string]$description
)
$aliases = get_config ALIAS ([PSCustomObject]@{})
if ($aliases.$name) {
abort "Alias '$name' already exists."
}
$alias_script_name = "scoop-$name"
$shimdir = shimdir $false
if (Test-Path "$shimdir\$alias_script_name.ps1") {
abort "File '$alias_script_name.ps1' already exists in shims directory."
}
$script = @(
"# Summary: $description",
"$command"
) -join "`n"
try {
$script | Out-UTF8File "$shimdir\$alias_script_name.ps1"
} catch {
abort $_.Exception
}
# Add the new alias to the config.
$aliases | Add-Member -MemberType NoteProperty -Name $name -Value $alias_script_name
set_config ALIAS $aliases | Out-Null
}
function rm_alias {
param(
[ValidateNotNullOrEmpty()]
[string]$name
)
$aliases = get_config ALIAS ([PSCustomObject]@{})
if (!$aliases.$name) {
abort "Alias '$name' doesn't exist."
}
info "Removing alias '$name'..."
if (Test-Path "$(shimdir $false)\scoop-$name.ps1") {
Remove-Item "$(shimdir $false)\scoop-$name.ps1"
}
$aliases.PSObject.Properties.Remove($name)
set_config ALIAS $aliases | Out-Null
}
function list_aliases {
param(
[bool]$verbose
)
$aliases = get_config ALIAS ([PSCustomObject]@{})
$alias_info = $aliases.PSObject.Properties.Name | Where-Object { $_ } | ForEach-Object {
# Mark the alias as <BROKEN>, if the alias script file does NOT exist.
if (!(Test-Path "$(shimdir $false)\scoop-$_.ps1")) {
[PSCustomObject]@{
Name = $_
Command = '<BROKEN>'
}
return
}
$content = Get-Content (command_path $_)
[PSCustomObject]@{
Name = $_
Command = ($content | Select-Object -Skip 1).Trim()
Summary = (summary $content).Trim()
}
}
if (!$alias_info) {
info 'No alias found.'
return
}
$alias_info = $alias_info | Sort-Object Name
$properties = @('Name', 'Command')
if ($verbose) {
$properties += 'Summary'
}
$alias_info | Select-Object $properties
}

View File

@@ -63,18 +63,6 @@ function Optimize-SecurityProtocol {
}
}
function Get-Encoding($wc) {
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
return [System.Text.Encoding]::GetEncoding($Matches[1])
} else {
return [System.Text.Encoding]::GetEncoding('utf-8')
}
}
function Get-UserAgent() {
return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if(${env:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
}
function Show-DeprecatedWarning {
<#
.SYNOPSIS
@@ -216,34 +204,15 @@ function Complete-ConfigChange {
}
}
}
}
function setup_proxy() {
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
$proxy = get_config PROXY
if(!$proxy) {
return
}
try {
$credentials, $address = $proxy -split '(?<!\\)@'
if(!$address) {
$address, $credentials = $credentials, $null # no credentials supplied
if ($Name -eq 'use_sqlite_cache' -and $Value -eq $true) {
if ((Get-DefaultArchitecture) -eq 'arm64') {
abort 'SQLite cache is not supported on ARM64 platform.'
}
if($address -eq 'none') {
[net.webrequest]::defaultwebproxy = $null
} elseif($address -ne 'default') {
[net.webrequest]::defaultwebproxy = new-object net.webproxy "http://$address"
}
if($credentials -eq 'currentuser') {
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
} elseif($credentials) {
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])','$1' }
[net.webrequest]::defaultwebproxy.credentials = new-object net.networkcredential($username, $password)
}
} catch {
warn "Failed to use proxy '$proxy': $($_.exception.message)"
. "$PSScriptRoot\..\lib\database.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
info 'Initializing SQLite cache in progress... This may take a while, please wait.'
Set-ScoopDB
}
}
@@ -314,10 +283,6 @@ function Invoke-GitLog {
# helper functions
function coalesce($a, $b) { if($a) { return $a } $b }
function format($str, $hash) {
$hash.keys | ForEach-Object { set-variable $_ $hash[$_] }
$executionContext.invokeCommand.expandString($str)
}
function is_admin {
$admin = [security.principal.windowsbuiltinrole]::administrator
$id = [security.principal.windowsidentity]::getcurrent()
@@ -399,7 +364,22 @@ function currentdir($app, $global) {
function persistdir($app, $global) { "$(basedir $global)\persist\$app" }
function usermanifestsdir { "$(basedir)\workspace" }
function usermanifest($app) { "$(usermanifestsdir)\$app.json" }
function cache_path($app, $version, $url) { "$cachedir\$app#$version#$($url -replace '[^\w\.\-]+', '_')" }
function cache_path($app, $version, $url) {
$underscoredUrl = $url -replace '[^\w\.\-]+', '_'
$filePath = Join-Path $cachedir "$app#$version#$underscoredUrl"
# NOTE: Scoop cache files migration. Remove this 6 months after the feature ships.
if (Test-Path $filePath) {
return $filePath
}
$urlStream = [System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($url))
$sha = (Get-FileHash -Algorithm SHA256 -InputStream $urlStream).Hash.ToLower().Substring(0, 7)
$extension = [System.IO.Path]::GetExtension($url)
$filePath = $filePath -replace "$underscoredUrl", "$sha$extension"
return $filePath
}
# apps
function sanitary_path($path) { return [regex]::replace($path, "[/\\?:*<>|]", "") }
@@ -477,7 +457,7 @@ function Get-HelperPath {
[OutputType([String])]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateSet('Git', '7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')]
[ValidateSet('Git', '7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2')]
[String]
$Helper
)
@@ -491,12 +471,17 @@ function Get-HelperPath {
if ($internalgit) {
$HelperPath = $internalgit
} else {
$HelperPath = (Get-Command git -ErrorAction Ignore).Source
$HelperPath = (Get-Command git -CommandType Application -TotalCount 1 -ErrorAction Ignore).Source
}
}
'7zip' { $HelperPath = Get-AppFilePath '7zip' '7z.exe' }
'Lessmsi' { $HelperPath = Get-AppFilePath 'lessmsi' 'lessmsi.exe' }
'Innounp' { $HelperPath = Get-AppFilePath 'innounp' 'innounp.exe' }
'Innounp' {
$HelperPath = Get-AppFilePath 'innounp-unicode' 'innounp.exe'
if ([String]::IsNullOrEmpty($HelperPath)) {
$HelperPath = Get-AppFilePath 'innounp' 'innounp.exe'
}
}
'Dark' {
$HelperPath = Get-AppFilePath 'wixtoolset' 'wix.exe'
if ([String]::IsNullOrEmpty($HelperPath)) {
@@ -504,7 +489,6 @@ function Get-HelperPath {
}
}
'Aria2' { $HelperPath = Get-AppFilePath 'aria2' 'aria2c.exe' }
'Zstd' { $HelperPath = Get-AppFilePath 'zstd' 'zstd.exe' }
}
return $HelperPath
@@ -551,7 +535,7 @@ function Test-HelperInstalled {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateSet('7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2', 'Zstd')]
[ValidateSet('7zip', 'Lessmsi', 'Innounp', 'Dark', 'Aria2')]
[String]
$Helper
)
@@ -559,10 +543,6 @@ function Test-HelperInstalled {
return ![String]::IsNullOrWhiteSpace((Get-HelperPath -Helper $Helper))
}
function Test-Aria2Enabled {
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
}
function app_status($app, $global) {
$status = @{}
$status.installed = installed $app $global
@@ -574,6 +554,9 @@ function app_status($app, $global) {
$status.failed = failed $app $global
$status.hold = ($install_info.hold -eq $true)
$deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
$status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
$manifest = manifest $app $install_info.bucket $install_info.url
$status.removed = (!$manifest)
if ($manifest.version) {
@@ -601,7 +584,6 @@ function app_status($app, $global) {
if ($deps) {
$status.missing_deps += , $deps
}
return $status
}
@@ -614,28 +596,6 @@ function fname($path) { split-path $path -leaf }
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
function strip_fragment($url) { $url -replace (new-object uri $url).fragment }
function url_filename($url) {
(split-path $url -leaf).split('?') | Select-Object -First 1
}
# Unlike url_filename which can be tricked by appending a
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
# this function extracts the original filename from the URL.
function url_remote_filename($url) {
$uri = (New-Object URI $url)
$basename = Split-Path $uri.PathAndQuery -Leaf
If ($basename -match ".*[?=]+([\w._-]+)") {
$basename = $matches[1]
}
If (($basename -notlike "*.*") -or ($basename -match "^[v.\d]+$")) {
$basename = Split-Path $uri.AbsolutePath -Leaf
}
If (($basename -notlike "*.*") -and ($uri.Fragment -ne "")) {
$basename = $uri.Fragment.Trim('/', '#')
}
return $basename
}
function ensure($dir) {
if (!(Test-Path -Path $dir)) {
New-Item -Path $dir -ItemType Directory | Out-Null
@@ -746,26 +706,35 @@ function Invoke-ExternalCommand {
$Process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
}
if ($ArgumentList.Length -gt 0) {
$ArgumentList = $ArgumentList | ForEach-Object { [regex]::Split($_.Replace('"', ''), '(?<=(?<![:\w])[/-]\w+) | (?=[/-])') }
if ($FilePath -match '^((cmd|cscript|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$') {
$Process.StartInfo.Arguments = $ArgumentList -join ' '
} elseif ($Process.StartInfo.ArgumentList.Add) {
# Remove existing double quotes and split arguments
# '(?<=(?<![:\w])[/-]\w+) ' matches a space after a command line switch starting with a slash ('/') or a hyphen ('-')
# The inner item '(?<![:\w])[/-]' matches a slash ('/') or a hyphen ('-') not preceded by a colon (':') or a word character ('\w')
# so that it must be a command line switch, otherwise, it would be a path (e.g. 'C:/Program Files') or other word (e.g. 'some-arg')
# ' (?=[/-])' matches a space followed by a slash ('/') or a hyphen ('-'), i.e. the space before a command line switch
$ArgumentList = $ArgumentList.ForEach({ $_ -replace '"' -split '(?<=(?<![:\w])[/-]\w+) | (?=[/-])' })
# Use legacy argument escaping for commands having non-standard behavior with regard to argument passing.
# `msiexec` requires some args like `TARGETDIR="C:\Program Files"`, which is non-standard, therefore we treat it as a legacy command.
# NSIS installer's '/D' param may not work with the ArgumentList property, so we need to escape arguments manually.
# ref-1: https://learn.microsoft.com/en-us/powershell/scripting/learn/experimental-features?view=powershell-7.4#psnativecommandargumentpassing
# ref-2: https://nsis.sourceforge.io/Docs/Chapter3.html
$LegacyCommand = $FilePath -match '^((cmd|cscript|find|sqlcmd|wscript|msiexec)(\.exe)?|.*\.(bat|cmd|js|vbs|wsf))$' -or
($ArgumentList -match '^/S$|^/D=[A-Z]:[\\/].*$').Length -eq 2
$SupportArgumentList = $Process.StartInfo.PSObject.Properties.Name -contains 'ArgumentList'
if ((-not $LegacyCommand) -and $SupportArgumentList) {
# ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+)
# ref-1: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0
# ref-2: https://docs.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.2#net-framework-vs-net-core
$ArgumentList | ForEach-Object { $Process.StartInfo.ArgumentList.Add($_) }
$ArgumentList.ForEach({ $Process.StartInfo.ArgumentList.Add($_) })
} else {
# escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
$escapedArgs = $ArgumentList | ForEach-Object {
# escape N consecutive backslash(es), which are followed by a double quote or at the end of the string, to 2N consecutive ones
$s = $_ -replace '(\\+)(""|$)', '$1$1$2'
# quote the path if it contains spaces and is not NSIS's '/D' argument
# ref: https://nsis.sourceforge.io/Docs/Chapter3.html
if ($s -match ' ' -and $s -notmatch '/D=[A-Z]:[\\/].*') {
$s -replace '([A-Z]:[\\/].*)', '"$1"'
} else {
$s
}
# Escape arguments manually in lower versions
$escapedArgs = switch -regex ($ArgumentList) {
# Quote paths starting with a drive letter
'(?<!/D=)[A-Z]:[\\/].*' { $_ -replace '([A-Z]:[\\/].*)', '"$1"'; continue }
# Do not quote paths if it is NSIS's '/D' argument
'/D=[A-Z]:[\\/].*' { $_; continue }
# Quote args with spaces
' ' { "`"$_`""; continue }
default { $_; continue }
}
$Process.StartInfo.Arguments = $escapedArgs -join ' '
}
@@ -1004,19 +973,20 @@ function shim($path, $global, $name, $arg) {
warn_on_overwrite "$shim.cmd" $path
@(
"@rem $resolved_path",
"@cd /d $(Split-Path $resolved_path -Parent)"
"@java -jar `"$resolved_path`" $arg %*"
"@pushd $(Split-Path $resolved_path -Parent)",
"@java -jar `"$resolved_path`" $arg %*",
"@popd"
) -join "`r`n" | Out-UTF8File "$shim.cmd"
warn_on_overwrite $shim $path
@(
"#!/bin/sh",
"# $resolved_path",
"if [ `$(echo `$WSL_DISTRO_NAME) ]",
"if [ `$WSL_INTEROP ]",
'then',
" cd `$(wslpath -u '$(Split-Path $resolved_path -Parent)')",
'else',
" cd `"$((Split-Path $resolved_path -Parent).Replace('\', '/'))`"",
" cd `$(cygpath -u '$(Split-Path $resolved_path -Parent)')",
'fi',
"java.exe -jar `"$resolved_path`" $arg `"$@`""
) -join "`n" | Out-UTF8File $shim -NoNewLine
@@ -1029,22 +999,37 @@ function shim($path, $global, $name, $arg) {
warn_on_overwrite $shim $path
@(
"#!/bin/sh",
'#!/bin/sh',
"# $resolved_path",
"python.exe `"$resolved_path`" $arg `"$@`""
) -join "`n" | Out-UTF8File $shim -NoNewLine
} else {
warn_on_overwrite "$shim.cmd" $path
$quoted_arg = if ($arg.Count -gt 0) { $arg | ForEach-Object { "`"$_`"" } }
@(
"@rem $resolved_path",
"@bash `"$resolved_path`" $arg %*"
'@echo off',
'bash -c "command -v wslpath >/dev/null"',
'if %errorlevel% equ 0 (',
" bash `"`$(wslpath -u '$resolved_path')`" $quoted_arg %*",
') else (',
" set args=$quoted_arg %*",
' setlocal enabledelayedexpansion',
' if not "!args!"=="" set args=!args:"=""!',
" bash -c `"`$(cygpath -u '$resolved_path') !args!`"",
')'
) -join "`r`n" | Out-UTF8File "$shim.cmd"
warn_on_overwrite $shim $path
@(
"#!/bin/sh",
'#!/bin/sh',
"# $resolved_path",
"`"$resolved_path`" $arg `"$@`""
"if [ `$WSL_INTEROP ]",
'then',
" `"`$(wslpath -u '$resolved_path')`" $arg `"$@`"",
'else',
" `"`$(cygpath -u '$resolved_path')`" $arg `"$@`"",
'fi'
) -join "`n" | Out-UTF8File $shim -NoNewLine
}
}
@@ -1163,7 +1148,7 @@ function applist($apps, $global) {
}
function parse_app([string]$app) {
if ($app -match '^(?:(?<bucket>[a-zA-Z0-9-_.]+)/)?(?<app>.*\.json$|[a-zA-Z0-9-_.]+)(?:@(?<version>.*))?$') {
if ($app -match '^(?:(?<bucket>[a-zA-Z0-9-_.]+)/)?(?<app>.*\.json|[a-zA-Z0-9-_.]+)(?:@(?<version>.*))?$') {
return $Matches['app'], $Matches['bucket'], $Matches['version']
} else {
return $app, $null, $null
@@ -1216,8 +1201,8 @@ function Test-ScoopCoreOnHold() {
}
function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
$newentity = $entity
if ($null -ne $newentity) {
if ($null -ne $entity) {
$newentity = $entity.PSObject.Copy()
switch ($entity.GetType().Name) {
'String' {
$params.GetEnumerator() | ForEach-Object {
@@ -1229,7 +1214,7 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
}
}
'Object[]' {
$newentity = $entity | ForEach-Object { ,(substitute $_ $params $regexEscape) }
$newentity = $entity | ForEach-Object { , (substitute $_ $params $regexEscape) }
}
'PSCustomObject' {
$newentity.PSObject.Properties | ForEach-Object { $_.Value = substitute $_.Value $params $regexEscape }
@@ -1239,112 +1224,6 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
return $newentity
}
function format_hash([String] $hash) {
$hash = $hash.toLower()
switch ($hash.Length)
{
32 { $hash = "md5:$hash" } # md5
40 { $hash = "sha1:$hash" } # sha1
64 { $hash = $hash } # sha256
128 { $hash = "sha512:$hash" } # sha512
default { $hash = $null }
}
return $hash
}
function format_hash_aria2([String] $hash) {
$hash = $hash -split ':' | Select-Object -Last 1
switch ($hash.Length)
{
32 { $hash = "md5=$hash" } # md5
40 { $hash = "sha-1=$hash" } # sha1
64 { $hash = "sha-256=$hash" } # sha256
128 { $hash = "sha-512=$hash" } # sha512
default { $hash = $null }
}
return $hash
}
function get_hash([String] $multihash) {
$type, $hash = $multihash -split ':'
if(!$hash) {
# no type specified, assume sha256
$type, $hash = 'sha256', $multihash
}
if(@('md5','sha1','sha256', 'sha512') -notcontains $type) {
return $null, "Hash type '$type' isn't supported."
}
return $type, $hash.ToLower()
}
function Get-GitHubToken {
return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1
}
function handle_special_urls($url)
{
# FossHub.com
if ($url -match "^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$") {
$Body = @{
projectUri = $Matches.name;
fileName = $Matches.filename;
source = 'CF';
isLatestVersion = $true
}
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
$Body.Add("projectId", $Matches.pid)
$Body.Add("releaseId", $Matches.rid)
}
$url = Invoke-RestMethod -Method Post -Uri "https://api.fosshub.com/download/" -ContentType "application/json" -Body (ConvertTo-Json $Body -Compress)
if ($null -eq $url.error) {
$url = $url.data.url
}
}
# Sourceforge.net
if ($url -match "(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)") {
# Reshapes the URL to avoid redirections
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
}
# Github.com
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
$headers = @{ "Authorization" = "token $token" }
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
}
}
return $url
}
function get_magic_bytes($file) {
if(!(Test-Path $file)) {
return ''
}
if((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
return Get-Content $file -AsByteStream -TotalCount 8
}
else {
return Get-Content $file -Encoding byte -TotalCount 8
}
}
function get_magic_bytes_pretty($file, $glue = ' ') {
if(!(Test-Path $file)) {
return ''
}
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
}
function Out-UTF8File {
param(
[Parameter(Mandatory = $True, Position = 0)]
@@ -1430,6 +1309,3 @@ $scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
# OS information
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
# Setup proxy globally
setup_proxy

410
lib/database.ps1 Normal file
View File

@@ -0,0 +1,410 @@
# Description: Functions for interacting with the Scoop database cache
<#
.SYNOPSIS
Get SQLite .NET driver
.DESCRIPTION
Download and extract the SQLite .NET driver from NuGet.
.PARAMETER Version
System.String
The version of the SQLite .NET driver to download.
.INPUTS
None
.OUTPUTS
System.Boolean
True if the SQLite .NET driver was successfully downloaded and extracted, otherwise false.
#>
function Get-SQLite {
param (
[string]$Version = '1.0.118'
)
# Install SQLite
try {
Write-Host "Downloading SQLite $Version..." -ForegroundColor DarkYellow
$sqlitePkgPath = "$env:TEMP\sqlite.zip"
$sqliteTempPath = "$env:TEMP\sqlite"
$sqlitePath = "$PSScriptRoot\..\supporting\sqlite"
Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/stub.system.data.sqlite.core.netframework/$version/stub.system.data.sqlite.core.netframework.$version.nupkg" -OutFile $sqlitePkgPath
Write-Host "Extracting SQLite $Version..." -ForegroundColor DarkYellow -NoNewline
Expand-Archive -Path $sqlitePkgPath -DestinationPath $sqliteTempPath -Force
New-Item -Path $sqlitePath -ItemType Directory -Force | Out-Null
Move-Item -Path "$sqliteTempPath\build\net451\*", "$sqliteTempPath\lib\net451\System.Data.SQLite.dll" -Destination $sqlitePath -Force
Remove-Item -Path $sqlitePkgPath, $sqliteTempPath -Recurse -Force
Write-Host ' Done' -ForegroundColor DarkYellow
return $true
} catch {
return $false
}
}
<#
.SYNOPSIS
Open Scoop SQLite database.
.DESCRIPTION
Open Scoop SQLite database connection and create the necessary tables if not exists.
.INPUTS
None
.OUTPUTS
System.Data.SQLite.SQLiteConnection
The SQLite database connection if **PassThru** is used.
#>
function Open-ScoopDB {
# Load System.Data.SQLite
if (!('System.Data.SQLite.SQLiteConnection' -as [Type])) {
try {
if (!(Test-Path -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll")) {
Get-SQLite | Out-Null
}
Add-Type -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll"
} catch {
throw "Scoop's Database cache requires the ADO.NET driver:`n`thttp://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki"
}
}
$dbPath = Join-Path $scoopdir 'scoop.db'
$db = New-Object -TypeName System.Data.SQLite.SQLiteConnection
$db.ConnectionString = "Data Source=$dbPath"
$db.ParseViaFramework = $true # Allow UNC path
$db.Open()
$tableCommand = $db.CreateCommand()
$tableCommand.CommandText = "CREATE TABLE IF NOT EXISTS 'app' (
name TEXT NOT NULL COLLATE NOCASE,
description TEXT NOT NULL,
version TEXT NOT NULL,
bucket VARCHAR NOT NULL,
manifest JSON NOT NULL,
binary TEXT,
shortcut TEXT,
dependency TEXT,
suggest TEXT,
PRIMARY KEY (name, version, bucket)
)"
$tableCommand.CommandType = [System.Data.CommandType]::Text
$tableCommand.ExecuteNonQuery() | Out-Null
$tableCommand.Dispose()
return $db
}
<#
.SYNOPSIS
Set Scoop database item(s).
.DESCRIPTION
Insert or replace item(s) into the Scoop SQLite database.
.PARAMETER InputObject
System.Object[]
The database item(s) to insert or replace.
.INPUTS
System.Object[]
.OUTPUTS
None
#>
function Set-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[psobject[]]
$InputObject
)
begin {
$db = Open-ScoopDB
$dbTrans = $db.BeginTransaction()
# TODO Support [hashtable]$InputObject
$colName = @($InputObject | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)
$dbQuery = "INSERT OR REPLACE INTO app ($($colName -join ', ')) VALUES ($('@' + ($colName -join ', @')))"
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
}
process {
foreach ($item in $InputObject) {
$item.PSObject.Properties | ForEach-Object {
$dbCommand.Parameters.AddWithValue("@$($_.Name)", $_.Value) | Out-Null
}
$dbCommand.ExecuteNonQuery() | Out-Null
}
}
end {
try {
$dbTrans.Commit()
} catch {
$dbTrans.Rollback()
throw $_
} finally {
$dbCommand.Dispose()
$dbTrans.Dispose()
$db.Dispose()
}
}
}
<#
.SYNOPSIS
Set Scoop app database item(s).
.DESCRIPTION
Insert or replace Scoop app(s) into the database.
.PARAMETER Path
System.String
The path to the bucket.
.PARAMETER CommitHash
System.String
The commit hash to compare with the HEAD.
.INPUTS
None
.OUTPUTS
None
#>
function Set-ScoopDB {
[CmdletBinding()]
param (
[Parameter(Position = 0, ValueFromPipeline)]
[string[]]
$Path
)
begin {
$list = [System.Collections.Generic.List[psobject]]::new()
$arch = Get-DefaultArchitecture
}
process {
if ($Path.Count -eq 0) {
$bucketPath = Get-LocalBucket | ForEach-Object { Find-BucketDirectory $_ }
$Path = (Get-ChildItem $bucketPath -Filter '*.json' -Recurse).FullName
}
$Path | ForEach-Object {
$manifestRaw = [System.IO.File]::ReadAllText($_)
$manifest = ConvertFrom-Json $manifestRaw -ErrorAction SilentlyContinue
if ($null -ne $manifest.version) {
$list.Add([pscustomobject]@{
name = $($_ -replace '.*[\\/]([^\\/]+)\.json$', '$1')
description = if ($manifest.description) { $manifest.description } else { '' }
version = $manifest.version
bucket = $($_ -replace '.*buckets[\\/]([^\\/]+)(?:[\\/].*)', '$1')
manifest = $manifestRaw
binary = $(
$result = @()
@(arch_specific 'bin' $manifest $arch) | ForEach-Object {
if ($_ -is [System.Array]) {
$result += "$($_[1]).$($_[0].Split('.')[-1])"
} else {
$result += $_
}
}
$result -replace '.*?([^\\/]+)?(\.(exe|bat|cmd|ps1|jar|py))$', '$1' -join ' | '
)
shortcut = $(
$result = @()
@(arch_specific 'shortcuts' $manifest $arch) | ForEach-Object {
$result += $_[1]
}
$result -replace '.*?([^\\/]+$)', '$1' -join ' | '
)
dependency = $manifest.depends -join ' | '
suggest = $(
$suggest_output = @()
$manifest.suggest.PSObject.Properties | ForEach-Object {
$suggest_output += $_.Value -join ' | '
}
$suggest_output -join ' | '
)
})
}
}
}
end {
if ($list.Count -ne 0) {
Set-ScoopDBItem $list
}
}
}
<#
.SYNOPSIS
Select Scoop database item(s).
.DESCRIPTION
Select item(s) from the Scoop SQLite database.
The pattern is matched against the name, binaries, and shortcuts columns for apps.
.PARAMETER Pattern
System.String
The pattern to search for. If is an empty string, all items will be returned.
.PARAMETER From
System.String[]
The fields to search from.
.INPUTS
System.String
.OUTPUTS
System.Data.DataTable
The selected database item(s).
#>
function Select-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[AllowEmptyString()]
[string]
$Pattern,
[Parameter(Mandatory, Position = 1)]
[ValidateNotNullOrEmpty()]
[string[]]
$From
)
begin {
$db = Open-ScoopDB
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
$result = New-Object System.Data.DataTable
$dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')"
$dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket"
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
$dbAdapter.SelectCommand = $dbCommand
}
process {
$dbCommand.Parameters.AddWithValue('@Pattern', $(if ($Pattern -eq '') { '%' } else { '%' + $Pattern + '%' })) | Out-Null
[void]$dbAdapter.Fill($result)
}
end {
$resultCopy = $result.Copy()
$dbAdapter.Dispose()
$db.Dispose()
# Use Write-Output to ensure PowerShell properly returns the DataTable
Write-Output $resultCopy -NoEnumerate
}
}
<#
.SYNOPSIS
Get Scoop database item.
.DESCRIPTION
Get item from the Scoop SQLite database.
.PARAMETER Name
System.String
The name of the item to get.
.PARAMETER Bucket
System.String
The bucket of the item to get.
.PARAMETER Version
System.String
The version of the item to get. If not provided, the latest version will be returned.
.INPUTS
System.String
.OUTPUTS
System.Data.DataTable
The selected database item.
#>
function Get-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]
$Name,
[Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
[string]
$Bucket,
[Parameter(Position = 2, ValueFromPipelineByPropertyName)]
[string]
$Version
)
begin {
$db = Open-ScoopDB
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
$dbCommand = $db.CreateCommand()
$dbCommand.CommandType = [System.Data.CommandType]::Text
$dbAdapter.SelectCommand = $dbCommand
}
process {
$result = New-Object System.Data.DataTable
# Build query based on whether version is specified
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
if ($Version) {
$dbQuery += ' AND version = @Version'
} else {
$dbQuery += ' ORDER BY version DESC LIMIT 1'
}
$dbCommand.CommandText = $dbQuery
$dbCommand.Parameters.Clear()
# Add parameters for this specific query
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
if ($Version) {
$dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null
}
[void]$dbAdapter.Fill($result)
# Create a copy of the DataTable to avoid disposal issues
$resultCopy = $result.Copy()
# Use Write-Output to ensure PowerShell properly returns the DataTable
Write-Output $resultCopy -NoEnumerate
}
end {
# Clean up all resources
$dbAdapter.Dispose()
$dbCommand.Dispose()
$db.Dispose()
}
}
<#
.SYNOPSIS
Remove Scoop database item(s).
.DESCRIPTION
Remove item(s) from the Scoop SQLite database.
.PARAMETER Name
System.String
The name of the item to remove.
.PARAMETER Bucket
System.String
The bucket of the item to remove.
.INPUTS
System.String
.OUTPUTS
None
#>
function Remove-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]
$Name,
[Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
[string]
$Bucket
)
begin {
$db = Open-ScoopDB
$dbTrans = $db.BeginTransaction()
$dbQuery = 'DELETE FROM app WHERE bucket = @Bucket'
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
}
process {
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
if ($Name) {
$dbCommand.CommandText = $dbQuery + ' AND name = @Name'
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
}
$dbCommand.ExecuteNonQuery() | Out-Null
}
end {
try {
$dbTrans.Commit()
} catch {
$dbTrans.Rollback()
throw $_
} finally {
$dbCommand.Dispose()
$dbTrans.Dispose()
$db.Dispose()
}
}
}

View File

@@ -1,3 +1,67 @@
# Description: Functions for decompressing archives or installers
function Invoke-Extraction {
param (
[string]
$Path,
[string[]]
$Name,
[psobject]
$Manifest,
[Alias('Arch', 'Architecture')]
[string]
$ProcessorArchitecture
)
$uri = @(url $Manifest $ProcessorArchitecture)
# 'extract_dir' and 'extract_to' are paired
$extractDir = @(extract_dir $Manifest $ProcessorArchitecture)
$extractTo = @(extract_to $Manifest $ProcessorArchitecture)
$extracted = 0
for ($i = 0; $i -lt $Name.Length; $i++) {
# work out extraction method, if applicable
$extractFn = $null
switch -regex ($Name[$i]) {
'\.zip$' {
if ((Test-HelperInstalled -Helper 7zip) -or ((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z))) {
$extractFn = 'Expand-7zipArchive'
} else {
$extractFn = 'Expand-ZipArchive'
}
continue
}
'\.msi$' {
$extractFn = 'Expand-MsiArchive'
continue
}
'\.exe$' {
if ($Manifest.innosetup) {
$extractFn = 'Expand-InnoArchive'
}
continue
}
{ Test-7zipRequirement -Uri $_ } {
$extractFn = 'Expand-7zipArchive'
continue
}
}
if ($extractFn) {
$fnArgs = @{
Path = Join-Path $Path $Name[$i]
DestinationPath = Join-Path $Path $extractTo[$extracted]
ExtractDir = $extractDir[$extracted]
}
Write-Host 'Extracting ' -NoNewline
Write-Host $(url_remote_filename $uri[$i]) -ForegroundColor Cyan -NoNewline
Write-Host ' ... ' -NoNewline
& $extractFn @fnArgs -Removal
Write-Host 'done.' -ForegroundColor Green
$extracted++
}
}
}
function Expand-7zipArchive {
[CmdletBinding()]
param (
@@ -59,15 +123,18 @@ function Expand-7zipArchive {
}
if (!$IsTar -and $ExtractDir) {
movedir "$DestinationPath\$ExtractDir" $DestinationPath | Out-Null
# Remove temporary directory
Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore
# Remove temporary directory if it is empty
$ExtractDirTopPath = [string] "$DestinationPath\$($ExtractDir -replace '[\\/].*')"
if ((Get-ChildItem -Path $ExtractDirTopPath -Force -ErrorAction Ignore).Count -eq 0) {
Remove-Item -Path $ExtractDirTopPath -Recurse -Force -ErrorAction Ignore
}
}
if (Test-Path $LogPath) {
Remove-Item $LogPath -Force
}
if ($Removal) {
if (($Path -replace '.*\.([^\.]*)$', '$1') -eq '001') {
# Remove splited 7-zip archive parts
# Remove splitted 7-zip archive parts
Get-ChildItem "$($Path -replace '\.[^\.]*$', '').???" | Remove-Item -Force
} elseif (($Path -replace '.*\.part(\d+)\.rar$', '$1')[-1] -eq '1') {
# Remove splitted RAR archive parts
@@ -96,37 +163,9 @@ function Expand-ZstdArchive {
[Switch]
$Removal
)
$ZstdPath = Get-HelperPath -Helper Zstd
$LogPath = Join-Path (Split-Path $Path) 'zstd.log'
$DestinationPath = $DestinationPath.TrimEnd('\')
ensure $DestinationPath | Out-Null
$ArgList = @('-d', $Path, '--output-dir-flat', $DestinationPath, '-f', '-v')
if ($Switches) {
$ArgList += (-split $Switches)
}
if ($Removal) {
# Remove original archive file
$ArgList += '--rm'
}
$Status = Invoke-ExternalCommand $ZstdPath $ArgList -LogPath $LogPath
if (!$Status) {
abort "Failed to extract files from $Path.`nLog file:`n $(friendly_path $LogPath)`n$(new_issue_msg $app $bucket 'decompress error')"
}
$IsTar = (strip_ext $Path) -match '\.tar$'
if ($IsTar) {
# Check for tar
$TarFile = Join-Path $DestinationPath (strip_ext (fname $Path))
Expand-7zipArchive -Path $TarFile -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Removal
}
if (!$IsTar -and $ExtractDir) {
movedir (Join-Path $DestinationPath $ExtractDir) $DestinationPath | Out-Null
# Remove temporary directory
Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore
}
if (Test-Path $LogPath) {
Remove-Item $LogPath -Force
}
# TODO: Remove this function after 2024/12/31
Show-DeprecatedWarning $MyInvocation 'Expand-7zipArchive'
Expand-7zipArchive -Path $Path -DestinationPath $DestinationPath -ExtractDir $ExtractDir -Switches $Switches -Removal:$Removal
}
function Expand-MsiArchive {
@@ -156,7 +195,7 @@ function Expand-MsiArchive {
$ArgList = @('x', $Path, "$DestinationPath\")
} else {
$MsiPath = 'msiexec.exe'
$ArgList = @('/a', "`"$Path`"", '/qn', "TARGETDIR=`"$DestinationPath\SourceDir`"")
$ArgList = @('/a', $Path, '/qn', "TARGETDIR=$DestinationPath\SourceDir")
}
$LogPath = "$(Split-Path $Path)\msi.log"
if ($Switches) {

View File

@@ -118,11 +118,8 @@ function Get-InstallationHelper {
if ($script -like '*Expand-DarkArchive *') {
$helper += 'dark'
}
if ((Test-ZstdRequirement -Uri $url) -or ($script -like '*Expand-ZstdArchive *')) {
$helper += 'zstd'
}
if (!$All) {
'7zip', 'lessmsi', 'innounp', 'dark', 'zstd' | ForEach-Object {
'7zip', 'lessmsi', 'innounp', 'dark' | ForEach-Object {
if (Test-HelperInstalled -Helper $_) {
$helper = $helper -ne $_
}
@@ -144,22 +141,10 @@ function Test-7zipRequirement {
$Uri
)
return ($Uri | Where-Object {
$_ -match '\.((gz)|(tar)|(t[abgpx]z2?)|(lzma)|(bz2?)|(7z)|(001)|(rar)|(iso)|(xz)|(lzh)|(nupkg))(\.[^\d.]+)?$'
$_ -match '\.(001|7z|bz(ip)?2?|gz|img|iso|lzma|lzh|nupkg|rar|tar|t[abgpx]z2?|t?zst|xz)(\.[^\d.]+)?$'
}).Count -gt 0
}
function Test-ZstdRequirement {
[CmdletBinding()]
[OutputType([Boolean])]
param (
[Parameter(Mandatory = $true)]
[AllowNull()]
[String[]]
$Uri
)
return ($Uri | Where-Object { $_ -match '\.zst$' }).Count -gt 0
}
function Test-LessmsiRequirement {
[CmdletBinding()]
[OutputType([Boolean])]

769
lib/download.ps1 Normal file
View File

@@ -0,0 +1,769 @@
# Description: Functions for downloading files
## Meta downloader
function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) {
# we only want to show this warning once
if (!$use_cache) { warn 'Cache is being ignored.' }
# can be multiple urls: if there are, then installer should go first to make 'installer.args' section work
$urls = @(script:url $manifest $architecture)
# can be multiple cookies: they will be used for all HTTP requests.
$cookies = $manifest.cookie
# download first
if (Test-Aria2Enabled) {
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
} else {
foreach ($url in $urls) {
$fname = url_filename $url
try {
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
} catch {
Write-Host -ForegroundColor DarkRed $_
abort "URL $url is not valid"
}
if ($check_hash) {
$manifest_hash = hash_for_url $manifest $url $architecture
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
if (!$ok) {
error $err
$cached = cache_path $app $version $url
if (Test-Path $cached) {
# rm cached file
Remove-Item -Force $cached
}
if ($url.Contains('sourceforge.net')) {
Write-Host -ForegroundColor Yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
abort $(new_issue_msg $app $bucket 'hash check failed')
}
}
}
}
return $urls.ForEach({ url_filename $_ })
}
## [System.Net] downloader
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
$cached = cache_path $app $version $url
if (!(Test-Path $cached) -or !$use_cache) {
ensure $cachedir | Out-Null
Start-Download $url "$cached.download" $cookies
Move-Item "$cached.download" $cached -Force
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
if (!($null -eq $to)) {
if ($use_cache) {
Copy-Item $cached $to
} else {
Move-Item $cached $to -Force
}
}
}
function Start-Download ($url, $to, $cookies) {
$progress = [console]::isoutputredirected -eq $false -and
$host.name -ne 'Windows PowerShell ISE Host'
try {
$url = handle_special_urls $url
Invoke-Download $url $to $cookies $progress
} catch {
$e = $_.exception
if ($e.Response.StatusCode -eq 'Unauthorized') {
warn 'Token might be misconfigured.'
}
if ($e.innerexception) { $e = $e.innerexception }
throw $e
}
}
function Invoke-Download ($url, $to, $cookies, $progress) {
# download with filesize and progress indicator
$reqUrl = ($url -split '#')[0]
$wreq = [Net.WebRequest]::Create($reqUrl)
if ($wreq -is [Net.HttpWebRequest]) {
$wreq.UserAgent = Get-UserAgent
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
$wreq.Referer = strip_filename $url
}
if ($url -match 'api\.github\.com/repos') {
$wreq.Accept = 'application/octet-stream'
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
}
if ($cookies) {
$wreq.Headers.Add('Cookie', (cookie_header $cookies))
}
get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object {
(ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object {
$wreq.Headers[$_.Key] = $_.Value
}
}
}
try {
$wres = $wreq.GetResponse()
} catch [System.Net.WebException] {
$exc = $_.Exception
$handledCodes = @(
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
[System.Net.HttpStatusCode]::Found, # HTTP 302
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
)
# Only handle redirection codes
$redirectRes = $exc.Response
if ($handledCodes -notcontains $redirectRes.StatusCode) {
throw $exc
}
# Get the new location of the file
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
throw $exc
}
$newUrl = $redirectRes.Headers['Location']
info "Following redirect to $newUrl..."
# Handle manual file rename
if ($url -like '*#/*') {
$null, $postfix = $url -split '#/'
$newUrl = "$newUrl`#/$postfix"
}
Invoke-Download $newUrl $to $cookies $progress
return
}
$total = $wres.ContentLength
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
$total = ftp_file_size($url)
}
if ($progress -and ($total -gt 0)) {
[console]::CursorVisible = $false
function Trace-DownloadProgress ($read) {
Write-DownloadProgress $read $total $url
}
} else {
Write-Host "Downloading $url ($(filesize $total))..."
function Trace-DownloadProgress {
#no op
}
}
try {
$s = $wres.getresponsestream()
$fs = [io.file]::openwrite($to)
$buffer = New-Object byte[] 2048
$totalRead = 0
$sw = [diagnostics.stopwatch]::StartNew()
Trace-DownloadProgress $totalRead
while (($read = $s.read($buffer, 0, $buffer.length)) -gt 0) {
$fs.write($buffer, 0, $read)
$totalRead += $read
if ($sw.elapsedmilliseconds -gt 100) {
$sw.restart()
Trace-DownloadProgress $totalRead
}
}
$sw.stop()
Trace-DownloadProgress $totalRead
} finally {
if ($progress) {
[console]::CursorVisible = $true
Write-Host
}
if ($fs) {
$fs.close()
}
if ($s) {
$s.close()
}
$wres.close()
}
}
function Format-DownloadProgress ($url, $read, $total, $console) {
$filename = url_remote_filename $url
# calculate current percentage done
$p = [math]::Round($read / $total * 100, 0)
# pre-generate LHS and RHS of progress string
# so we know how much space we have
$left = "$filename ($(filesize $total))"
$right = [string]::Format('{0,3}%', $p)
# calculate remaining width for progress bar
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
# calculate how many characters are completed
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
# generate dashes to symbolise completed
if ($completed -gt 1) {
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
}
# this is why we calculate $completed - 1 above
$dashes += switch ($p) {
100 { '=' }
default { '>' }
}
# the remaining characters are filled with spaces
$spaces = switch ($dashes.Length) {
$midwidth { [string]::Empty }
default {
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
}
}
"$left [$dashes$spaces] $right"
}
function Write-DownloadProgress ($read, $total, $url) {
$console = $Host.UI.RawUI
$left = $console.CursorPosition.X
$top = $console.CursorPosition.Y
$width = $console.BufferSize.Width
if ($read -eq 0) {
$maxOutputLength = $(Format-DownloadProgress $url 100 $total $console).Length
if (($left + $maxOutputLength) -gt $width) {
# not enough room to print progress on this line
# print on new line
Write-Host
$left = 0
$top = $top + 1
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
}
}
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
[console]::SetCursorPosition($left, $top)
}
## Aria2 downloader
function Test-Aria2Enabled {
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
}
function aria_exit_code($exitcode) {
$codes = @{
0 = 'All downloads were successful'
1 = 'An unknown error occurred'
2 = 'Timeout'
3 = 'Resource was not found'
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
6 = 'Network problem occurred.'
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
8 = 'Remote server did not support resume when resume was required to complete download'
9 = 'There was not enough disk space available'
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
11 = 'Aria2 was downloading same file at that moment'
12 = 'Aria2 was downloading same info hash torrent at that moment'
13 = 'File already existed. See --allow-overwrite option'
14 = 'Renaming file failed. See --auto-file-renaming option'
15 = 'Aria2 could not open existing file'
16 = 'Aria2 could not create new file or truncate existing file'
17 = 'File I/O error occurred'
18 = 'Aria2 could not create directory'
19 = 'Name resolution failed'
20 = 'Aria2 could not parse Metalink document'
21 = 'FTP command failed'
22 = 'HTTP response header was bad or unexpected'
23 = 'Too many redirects occurred'
24 = 'HTTP authorization failed'
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
26 = '".torrent" file was corrupted or missing information that aria2 needed'
27 = 'Magnet URI was bad'
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
30 = 'Aria2 could not parse JSON-RPC request'
31 = 'Reserved. Not used'
32 = 'Checksum validation failed'
}
if ($null -eq $codes[$exitcode]) {
return 'An unknown error occurred'
}
return $codes[$exitcode]
}
function get_filename_from_metalink($file) {
$bytes = get_magic_bytes_pretty $file ''
# check if file starts with '<?xml'
if (!($bytes.StartsWith('3c3f786d6c'))) {
return $null
}
# Add System.Xml for reading metalink files
Add-Type -AssemblyName 'System.Xml'
$xr = [System.Xml.XmlReader]::Create($file)
$filename = $null
try {
$xr.ReadStartElement('metalink')
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
$filename = $xr.Value
}
} catch [System.Xml.XmlException] {
return $null
} finally {
$xr.Close()
}
return $filename
}
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
$data = @{}
$urls = @(script:url $manifest $architecture)
# aria2 input file
$urlstxt = Join-Path $cachedir "$app.txt"
$urlstxt_content = ''
$download_finished = $true
# aria2 options
$options = @(
"--input-file='$urlstxt'"
"--user-agent='$(Get-UserAgent)'"
'--allow-overwrite=true'
'--auto-file-renaming=false'
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
"--split=$(get_config 'aria2-split' 5)"
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
'--console-log-level=warn'
'--enable-color=false'
'--no-conf=true'
'--follow-metalink=true'
'--metalink-preferred-protocol=https'
'--min-tls-version=TLSv1.2'
"--stop-with-process=$PID"
'--continue'
'--summary-interval=0'
'--auto-save-interval=1'
)
if ($cookies) {
$options += "--header='Cookie: $(cookie_header $cookies)'"
}
$proxy = get_config PROXY
if ($proxy -ne 'none') {
if ([Net.Webrequest]::DefaultWebProxy.Address) {
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
}
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
}
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
}
}
$more_options = get_config 'aria2-options'
if ($more_options) {
$options += $more_options
}
foreach ($url in $urls) {
$data.$url = @{
'target' = Join-Path $dir (url_filename $url)
'cachename' = fname (cache_path $app $version $url)
'source' = cache_path $app $version $url
}
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
Write-Host 'Loading ' -NoNewline
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
Write-Host ' from cache.'
} else {
$download_finished = $false
# create aria2 input file content
try {
$try_url = handle_special_urls $url
} catch {
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
warn 'Token might be misconfigured.'
}
}
$urlstxt_content += "$try_url`n"
if (!$url.Contains('sourceforge.net')) {
$urlstxt_content += " referer=$(strip_filename $url)`n"
}
$urlstxt_content += " dir=$cachedir`n"
$urlstxt_content += " out=$($data.$url.cachename)`n"
}
}
if (-not($download_finished)) {
# write aria2 input file
if ($urlstxt_content -ne '') {
ensure $cachedir | Out-Null
# Write aria2 input-file with UTF8NoBOM encoding
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
}
# build aria2 command
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
# handle aria2 console output
Write-Host 'Starting download with aria2 ...'
# Set console output encoding to UTF8 for non-ASCII characters printing
$oriConsoleEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
# Skip blank lines
if ([String]::IsNullOrWhiteSpace($_)) { return }
# Prevent potential overlaping of text when one line is shorter
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
$color = 'Gray'
if ($_.StartsWith('(OK):')) {
$noNewLine = $true
$color = 'Green'
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
$noNewLine = $true
$color = 'Cyan'
} elseif ($_.StartsWith('Download Results:')) {
$noNewLine = $false
}
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
}
Write-Host ''
if ($lastexitcode -gt 0) {
warn "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
warn $urlstxt_content
warn $aria2
warn $(new_issue_msg $app $bucket "download via aria2 failed")
Write-Host "Fallback to default downloader ..."
try {
foreach ($url in $urls) {
Invoke-CachedDownload $app $version $url "$($data.$url.target)" $cookies $use_cache
}
} catch {
Write-Host $_ -ForegroundColor DarkRed
abort "URL $url is not valid"
}
}
# remove aria2 input file when done
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
}
# Revert console encoding
[Console]::OutputEncoding = $oriConsoleEncoding
}
foreach ($url in $urls) {
$metalink_filename = get_filename_from_metalink $data.$url.source
if ($metalink_filename) {
Remove-Item $data.$url.source -Force
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
}
# run hash checks
if ($check_hash) {
$manifest_hash = hash_for_url $manifest $url $architecture
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
if (!$ok) {
error $err
if (Test-Path $data.$url.source) {
# rm cached file
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
}
if ($url.Contains('sourceforge.net')) {
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
abort $(new_issue_msg $app $bucket 'hash check failed')
}
}
# copy or move file to target location
if (!(Test-Path $data.$url.source) ) {
abort $(new_issue_msg $app $bucket 'cached file not found')
}
if (!($dir -eq $cachedir)) {
if ($use_cache) {
Copy-Item $data.$url.source $data.$url.target
} else {
Move-Item $data.$url.source $data.$url.target -Force
}
}
}
}
## Helper functions
### Downloader parameters
function cookie_header($cookies) {
if (!$cookies) { return }
$vals = $cookies.psobject.properties | ForEach-Object {
"$($_.name)=$($_.value)"
}
[string]::join(';', $vals)
}
function Get-Encoding($wc) {
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
return [System.Text.Encoding]::GetEncoding($Matches[1])
} else {
return [System.Text.Encoding]::GetEncoding('utf-8')
}
}
function Get-UserAgent() {
return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if(${env:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
}
function setup_proxy() {
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
$proxy = get_config PROXY
if (!$proxy) {
return
}
try {
$credentials, $address = $proxy -split '(?<!\\)@'
if (!$address) {
$address, $credentials = $credentials, $null # no credentials supplied
}
if ($address -eq 'none') {
[net.webrequest]::defaultwebproxy = $null
} elseif ($address -ne 'default') {
[net.webrequest]::defaultwebproxy = New-Object net.webproxy "http://$address"
}
if ($credentials -eq 'currentuser') {
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
} elseif ($credentials) {
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])', '$1' }
[net.webrequest]::defaultwebproxy.credentials = New-Object net.networkcredential($username, $password)
}
} catch {
warn "Failed to use proxy '$proxy': $($_.exception.message)"
}
}
function Get-GitHubToken {
return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1
}
function github_ratelimit_reached {
$api_link = 'https://api.github.com/rate_limit'
$ret = (download_json $api_link).rate.remaining -eq 0
if ($ret) {
Write-Host "GitHub API rate limit reached.`r`nPlease try again later or configure your API token using 'scoop config gh_token <your token>'."
}
$ret
}
### URL handling
function handle_special_urls($url) {
# FossHub.com
if ($url -match '^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$') {
$Body = @{
projectUri = $Matches.name
fileName = $Matches.filename
source = 'CF'
isLatestVersion = $true
}
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
$Body.Add('projectId', $Matches.pid)
$Body.Add('releaseId', $Matches.rid)
}
$url = Invoke-RestMethod -Method Post -Uri 'https://api.fosshub.com/download/' -ContentType 'application/json' -Body (ConvertTo-Json $Body -Compress)
if ($null -eq $url.error) {
$url = $url.data.url
}
}
# Sourceforge.net
if ($url -match '(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)') {
# Reshapes the URL to avoid redirections
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
}
# Github.com
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
$headers = @{ 'Authorization' = "token $token" }
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
}
}
return $url
}
### Remote file information
function download_json($url) {
$githubtoken = Get-GitHubToken
$authheader = @{}
if ($githubtoken) {
$authheader = @{'Authorization' = "token $githubtoken" }
}
$ProgressPreference = 'SilentlyContinue'
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
$ProgressPreference = 'Continue'
$result
}
function get_magic_bytes($file) {
if (!(Test-Path $file)) {
return ''
}
if ((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
return Get-Content $file -AsByteStream -TotalCount 8
} else {
return Get-Content $file -Encoding byte -TotalCount 8
}
}
function get_magic_bytes_pretty($file, $glue = ' ') {
if (!(Test-Path $file)) {
return ''
}
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
}
Function Get-RemoteFileSize ($Uri) {
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
if (!$response.Headers.StatusCode) {
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
}
}
function ftp_file_size($url) {
$request = [net.ftpwebrequest]::create($url)
$request.method = [net.webrequestmethods+ftp]::getfilesize
$request.getresponse().contentlength
}
function url_filename($url) {
(Split-Path $url -Leaf).split('?') | Select-Object -First 1
}
function url_remote_filename($url) {
# Unlike url_filename which can be tricked by appending a
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
# this function extracts the original filename from the URL.
$uri = (New-Object URI $url)
$basename = Split-Path $uri.PathAndQuery -Leaf
If ($basename -match '.*[?=]+([\w._-]+)') {
$basename = $matches[1]
}
If (($basename -notlike '*.*') -or ($basename -match '^[v.\d]+$')) {
$basename = Split-Path $uri.AbsolutePath -Leaf
}
If (($basename -notlike '*.*') -and ($uri.Fragment -ne '')) {
$basename = $uri.Fragment.Trim('/', '#')
}
return $basename
}
### Hash-related functions
function hash_for_url($manifest, $url, $arch) {
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
if ($hashes.length -eq 0) { return $null }
$urls = @(script:url $manifest $arch)
$index = [array]::IndexOf($urls, $url)
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
@($hashes)[$index]
}
function check_hash($file, $hash, $app_name) {
# returns (ok, err)
if (!$hash) {
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
return $true, $null
}
Write-Host 'Checking hash of ' -NoNewline
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
Write-Host ' ... ' -NoNewline
$algorithm, $expected = get_hash $hash
if ($null -eq $algorithm) {
return $false, "Hash type '$algorithm' isn't supported."
}
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
$expected = $expected.ToLower()
if ($actual -ne $expected) {
$msg = "Hash check failed!`n"
$msg += "App: $app_name`n"
$msg += "URL: $url`n"
if (Test-Path $file) {
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
}
if ($expected -or $actual) {
$msg += "Expected: $expected`n"
$msg += "Actual: $actual"
}
return $false, $msg
}
Write-Host 'ok.' -f Green
return $true, $null
}
function get_hash([String] $multihash) {
$type, $hash = $multihash -split ':'
if (!$hash) {
# no type specified, assume sha256
$type, $hash = 'sha256', $multihash
}
if (@('md5', 'sha1', 'sha256', 'sha512') -notcontains $type) {
return $null, "Hash type '$type' isn't supported."
}
return $type, $hash.ToLower()
}
# Setup proxy globally
setup_proxy

View File

@@ -50,22 +50,23 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
$persist_dir = persistdir $app $global
$fname = Invoke-ScoopDownload $app $version $manifest $bucket $architecture $dir $use_cache $check_hash
Invoke-HookScript -HookType 'pre_install' -Manifest $manifest -Arch $architecture
Invoke-Extraction -Path $dir -Name $fname -Manifest $manifest -ProcessorArchitecture $architecture
Invoke-HookScript -HookType 'pre_install' -Manifest $manifest -ProcessorArchitecture $architecture
run_installer $fname $manifest $architecture $dir $global
Invoke-Installer -Path $dir -Name $fname -Manifest $manifest -ProcessorArchitecture $architecture -AppName $app -Global:$global
ensure_install_dir_not_in_path $dir $global
$dir = link_current $dir
create_shims $manifest $dir $global $architecture
create_startmenu_shortcuts $manifest $dir $global $architecture
install_psmodule $manifest $dir $global
env_add_path $manifest $dir $global $architecture
env_set $manifest $dir $global $architecture
env_set $manifest $global $architecture
# persist data
persist_data $manifest $original_dir $persist_dir
persist_permission $manifest $global
Invoke-HookScript -HookType 'post_install' -Manifest $manifest -Arch $architecture
Invoke-HookScript -HookType 'post_install' -Manifest $manifest -ProcessorArchitecture $architecture
# save info for uninstall
save_installed_manifest $app $bucket $dir $url
@@ -80,687 +81,94 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
show_notes $manifest $dir $original_dir $persist_dir
}
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
$cached = cache_path $app $version $url
if (!(Test-Path $cached) -or !$use_cache) {
ensure $cachedir | Out-Null
Start-Download $url "$cached.download" $cookies
Move-Item "$cached.download" $cached -Force
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
if (!($null -eq $to)) {
if ($use_cache) {
Copy-Item $cached $to
} else {
Move-Item $cached $to -Force
}
}
}
function Start-Download ($url, $to, $cookies) {
$progress = [console]::isoutputredirected -eq $false -and
$host.name -ne 'Windows PowerShell ISE Host'
try {
$url = handle_special_urls $url
Invoke-Download $url $to $cookies $progress
} catch {
$e = $_.exception
if ($e.Response.StatusCode -eq 'Unauthorized') {
warn 'Token might be misconfigured.'
}
if ($e.innerexception) { $e = $e.innerexception }
throw $e
}
}
function aria_exit_code($exitcode) {
$codes = @{
0 = 'All downloads were successful'
1 = 'An unknown error occurred'
2 = 'Timeout'
3 = 'Resource was not found'
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
6 = 'Network problem occurred.'
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
8 = 'Remote server did not support resume when resume was required to complete download'
9 = 'There was not enough disk space available'
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
11 = 'Aria2 was downloading same file at that moment'
12 = 'Aria2 was downloading same info hash torrent at that moment'
13 = 'File already existed. See --allow-overwrite option'
14 = 'Renaming file failed. See --auto-file-renaming option'
15 = 'Aria2 could not open existing file'
16 = 'Aria2 could not create new file or truncate existing file'
17 = 'File I/O error occurred'
18 = 'Aria2 could not create directory'
19 = 'Name resolution failed'
20 = 'Aria2 could not parse Metalink document'
21 = 'FTP command failed'
22 = 'HTTP response header was bad or unexpected'
23 = 'Too many redirects occurred'
24 = 'HTTP authorization failed'
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
26 = '".torrent" file was corrupted or missing information that aria2 needed'
27 = 'Magnet URI was bad'
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
30 = 'Aria2 could not parse JSON-RPC request'
31 = 'Reserved. Not used'
32 = 'Checksum validation failed'
}
if ($null -eq $codes[$exitcode]) {
return 'An unknown error occurred'
}
return $codes[$exitcode]
}
function get_filename_from_metalink($file) {
$bytes = get_magic_bytes_pretty $file ''
# check if file starts with '<?xml'
if (!($bytes.StartsWith('3c3f786d6c'))) {
return $null
}
# Add System.Xml for reading metalink files
Add-Type -AssemblyName 'System.Xml'
$xr = [System.Xml.XmlReader]::Create($file)
$filename = $null
try {
$xr.ReadStartElement('metalink')
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
$filename = $xr.Value
}
} catch [System.Xml.XmlException] {
return $null
} finally {
$xr.Close()
}
return $filename
}
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
$data = @{}
$urls = @(script:url $manifest $architecture)
# aria2 input file
$urlstxt = Join-Path $cachedir "$app.txt"
$urlstxt_content = ''
$download_finished = $true
# aria2 options
$options = @(
"--input-file='$urlstxt'"
"--user-agent='$(Get-UserAgent)'"
'--allow-overwrite=true'
'--auto-file-renaming=false'
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
"--split=$(get_config 'aria2-split' 5)"
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
'--console-log-level=warn'
'--enable-color=false'
'--no-conf=true'
'--follow-metalink=true'
'--metalink-preferred-protocol=https'
'--min-tls-version=TLSv1.2'
"--stop-with-process=$PID"
'--continue'
'--summary-interval=0'
'--auto-save-interval=1'
)
if ($cookies) {
$options += "--header='Cookie: $(cookie_header $cookies)'"
}
$proxy = get_config PROXY
if ($proxy -ne 'none') {
if ([Net.Webrequest]::DefaultWebProxy.Address) {
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
}
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
}
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
}
}
$more_options = get_config 'aria2-options'
if ($more_options) {
$options += $more_options
}
foreach ($url in $urls) {
$data.$url = @{
'target' = "$dir\$(url_filename $url)"
'cachename' = fname (cache_path $app $version $url)
'source' = cache_path $app $version $url
}
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
Write-Host 'Loading ' -NoNewline
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
Write-Host ' from cache.'
} else {
$download_finished = $false
# create aria2 input file content
try {
$try_url = handle_special_urls $url
} catch {
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
warn 'Token might be misconfigured.'
}
}
$urlstxt_content += "$try_url`n"
if (!$url.Contains('sourceforge.net')) {
$urlstxt_content += " referer=$(strip_filename $url)`n"
}
$urlstxt_content += " dir=$cachedir`n"
$urlstxt_content += " out=$($data.$url.cachename)`n"
}
}
if (-not($download_finished)) {
# write aria2 input file
if ($urlstxt_content -ne '') {
ensure $cachedir | Out-Null
# Write aria2 input-file with UTF8NoBOM encoding
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
}
# build aria2 command
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
# handle aria2 console output
Write-Host 'Starting download with aria2 ...'
# Set console output encoding to UTF8 for non-ASCII characters printing
$oriConsoleEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
# Skip blank lines
if ([String]::IsNullOrWhiteSpace($_)) { return }
# Prevent potential overlaping of text when one line is shorter
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
$color = 'Gray'
if ($_.StartsWith('(OK):')) {
$noNewLine = $true
$color = 'Green'
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
$noNewLine = $true
$color = 'Cyan'
} elseif ($_.StartsWith('Download Results:')) {
$noNewLine = $false
}
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
}
Write-Host ''
if ($lastexitcode -gt 0) {
error "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
error $urlstxt_content
error $aria2
abort $(new_issue_msg $app $bucket 'download via aria2 failed')
}
# remove aria2 input file when done
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
}
# Revert console encoding
[Console]::OutputEncoding = $oriConsoleEncoding
}
foreach ($url in $urls) {
$metalink_filename = get_filename_from_metalink $data.$url.source
if ($metalink_filename) {
Remove-Item $data.$url.source -Force
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
}
# run hash checks
if ($check_hash) {
$manifest_hash = hash_for_url $manifest $url $architecture
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
if (!$ok) {
error $err
if (Test-Path $data.$url.source) {
# rm cached file
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
}
if ($url.Contains('sourceforge.net')) {
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
abort $(new_issue_msg $app $bucket 'hash check failed')
}
}
# copy or move file to target location
if (!(Test-Path $data.$url.source) ) {
abort $(new_issue_msg $app $bucket 'cached file not found')
}
if (!($dir -eq $cachedir)) {
if ($use_cache) {
Copy-Item $data.$url.source $data.$url.target
} else {
Move-Item $data.$url.source $data.$url.target -Force
}
}
}
}
# download with filesize and progress indicator
function Invoke-Download ($url, $to, $cookies, $progress) {
$reqUrl = ($url -split '#')[0]
$wreq = [Net.WebRequest]::Create($reqUrl)
if ($wreq -is [Net.HttpWebRequest]) {
$wreq.UserAgent = Get-UserAgent
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
$wreq.Referer = strip_filename $url
}
if ($url -match 'api\.github\.com/repos') {
$wreq.Accept = 'application/octet-stream'
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
}
if ($cookies) {
$wreq.Headers.Add('Cookie', (cookie_header $cookies))
}
get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object {
(ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object {
$wreq.Headers[$_.Key] = $_.Value
}
}
}
try {
$wres = $wreq.GetResponse()
} catch [System.Net.WebException] {
$exc = $_.Exception
$handledCodes = @(
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
[System.Net.HttpStatusCode]::Found, # HTTP 302
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
)
# Only handle redirection codes
$redirectRes = $exc.Response
if ($handledCodes -notcontains $redirectRes.StatusCode) {
throw $exc
}
# Get the new location of the file
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
throw $exc
}
$newUrl = $redirectRes.Headers['Location']
info "Following redirect to $newUrl..."
# Handle manual file rename
if ($url -like '*#/*') {
$null, $postfix = $url -split '#/'
$newUrl = "$newUrl#/$postfix"
}
Invoke-Download $newUrl $to $cookies $progress
return
}
$total = $wres.ContentLength
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
$total = ftp_file_size($url)
}
if ($progress -and ($total -gt 0)) {
[console]::CursorVisible = $false
function Trace-DownloadProgress ($read) {
Write-DownloadProgress $read $total $url
}
} else {
Write-Host "Downloading $url ($(filesize $total))..."
function Trace-DownloadProgress {
#no op
}
}
try {
$s = $wres.getresponsestream()
$fs = [io.file]::openwrite($to)
$buffer = New-Object byte[] 2048
$totalRead = 0
$sw = [diagnostics.stopwatch]::StartNew()
Trace-DownloadProgress $totalRead
while (($read = $s.read($buffer, 0, $buffer.length)) -gt 0) {
$fs.write($buffer, 0, $read)
$totalRead += $read
if ($sw.elapsedmilliseconds -gt 100) {
$sw.restart()
Trace-DownloadProgress $totalRead
}
}
$sw.stop()
Trace-DownloadProgress $totalRead
} finally {
if ($progress) {
[console]::CursorVisible = $true
Write-Host
}
if ($fs) {
$fs.close()
}
if ($s) {
$s.close()
}
$wres.close()
}
}
function Format-DownloadProgress ($url, $read, $total, $console) {
$filename = url_remote_filename $url
# calculate current percentage done
$p = [math]::Round($read / $total * 100, 0)
# pre-generate LHS and RHS of progress string
# so we know how much space we have
$left = "$filename ($(filesize $total))"
$right = [string]::Format('{0,3}%', $p)
# calculate remaining width for progress bar
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
# calculate how many characters are completed
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
# generate dashes to symbolise completed
if ($completed -gt 1) {
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
}
# this is why we calculate $completed - 1 above
$dashes += switch ($p) {
100 { '=' }
default { '>' }
}
# the remaining characters are filled with spaces
$spaces = switch ($dashes.Length) {
$midwidth { [string]::Empty }
default {
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
}
}
"$left [$dashes$spaces] $right"
}
function Write-DownloadProgress ($read, $total, $url) {
$console = $host.UI.RawUI
$left = $console.CursorPosition.X
$top = $console.CursorPosition.Y
$width = $console.BufferSize.Width
if ($read -eq 0) {
$maxOutputLength = $(Format-DownloadProgress $url 100 $total $console).length
if (($left + $maxOutputLength) -gt $width) {
# not enough room to print progress on this line
# print on new line
Write-Host
$left = 0
$top = $top + 1
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
}
}
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
[console]::SetCursorPosition($left, $top)
}
function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) {
# we only want to show this warning once
if (!$use_cache) { warn 'Cache is being ignored.' }
# can be multiple urls: if there are, then installer should go last,
# so that $fname is set properly
$urls = @(script:url $manifest $architecture)
# can be multiple cookies: they will be used for all HTTP requests.
$cookies = $manifest.cookie
$fname = $null
# extract_dir and extract_to in manifest are like queues: for each url that
# needs to be extracted, will get the next dir from the queue
$extract_dirs = @(extract_dir $manifest $architecture)
$extract_tos = @(extract_to $manifest $architecture)
$extracted = 0
# download first
if (Test-Aria2Enabled) {
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
} else {
foreach ($url in $urls) {
$fname = url_filename $url
try {
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
} catch {
Write-Host -f darkred $_
abort "URL $url is not valid"
}
if ($check_hash) {
$manifest_hash = hash_for_url $manifest $url $architecture
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
if (!$ok) {
error $err
$cached = cache_path $app $version $url
if (Test-Path $cached) {
# rm cached file
Remove-Item -Force $cached
}
if ($url.Contains('sourceforge.net')) {
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
abort $(new_issue_msg $app $bucket 'hash check failed')
}
}
}
}
foreach ($url in $urls) {
$fname = url_filename $url
$extract_dir = $extract_dirs[$extracted]
$extract_to = $extract_tos[$extracted]
# work out extraction method, if applicable
$extract_fn = $null
if ($manifest.innosetup) {
$extract_fn = 'Expand-InnoArchive'
} elseif ($fname -match '\.zip$') {
# Use 7zip when available (more fast)
if (((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z)) -or (Test-HelperInstalled -Helper 7zip)) {
$extract_fn = 'Expand-7zipArchive'
} else {
$extract_fn = 'Expand-ZipArchive'
}
} elseif ($fname -match '\.msi$') {
$extract_fn = 'Expand-MsiArchive'
} elseif (Test-ZstdRequirement -Uri $fname) {
# Zstd first
$extract_fn = 'Expand-ZstdArchive'
} elseif (Test-7zipRequirement -Uri $fname) {
# 7zip
$extract_fn = 'Expand-7zipArchive'
}
if ($extract_fn) {
Write-Host 'Extracting ' -NoNewline
Write-Host $fname -f Cyan -NoNewline
Write-Host ' ... ' -NoNewline
& $extract_fn -Path "$dir\$fname" -DestinationPath "$dir\$extract_to" -ExtractDir $extract_dir -Removal
Write-Host 'done.' -f Green
$extracted++
}
}
$fname # returns the last downloaded file
}
function cookie_header($cookies) {
if (!$cookies) { return }
$vals = $cookies.psobject.properties | ForEach-Object {
"$($_.name)=$($_.value)"
}
[string]::join(';', $vals)
}
function is_in_dir($dir, $check) {
$check -match "^$([regex]::Escape("$dir"))([/\\]|$)"
}
function ftp_file_size($url) {
$request = [net.ftpwebrequest]::create($url)
$request.method = [net.webrequestmethods+ftp]::getfilesize
$request.getresponse().contentlength
}
# hashes
function hash_for_url($manifest, $url, $arch) {
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
if ($hashes.length -eq 0) { return $null }
$urls = @(script:url $manifest $arch)
$index = [array]::indexof($urls, $url)
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
@($hashes)[$index]
}
# returns (ok, err)
function check_hash($file, $hash, $app_name) {
if (!$hash) {
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
return $true, $null
}
Write-Host 'Checking hash of ' -NoNewline
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
Write-Host ' ... ' -NoNewline
$algorithm, $expected = get_hash $hash
if ($null -eq $algorithm) {
return $false, "Hash type '$algorithm' isn't supported."
}
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
$expected = $expected.ToLower()
if ($actual -ne $expected) {
$msg = "Hash check failed!`n"
$msg += "App: $app_name`n"
$msg += "URL: $url`n"
if (Test-Path $file) {
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
function Invoke-Installer {
[CmdletBinding()]
param (
[string]
$Path,
[string[]]
$Name,
[psobject]
$Manifest,
[Alias('Arch', 'Architecture')]
[ValidateSet('32bit', '64bit', 'arm64')]
[string]
$ProcessorArchitecture,
[string]
$AppName,
[switch]
$Global,
[switch]
$Uninstall
)
$type = if ($Uninstall) { 'uninstaller' } else { 'installer' }
$installer = arch_specific $type $Manifest $ProcessorArchitecture
if ($installer.file -or $installer.args) {
# Installer filename is either explicit defined ('installer.file') or file name in the first URL
if (!$Name) {
$Name = url_filename @(url $manifest $architecture)
}
if ($expected -or $actual) {
$msg += "Expected: $expected`n"
$msg += "Actual: $actual"
$progName = "$Path\$(coalesce $installer.file $Name[0])"
if (!(is_in_dir $Path $progName)) {
abort "Error in manifest: $((Get-Culture).TextInfo.ToTitleCase($type)) $progName is outside the app directory."
} elseif (!(Test-Path $progName)) {
abort "$((Get-Culture).TextInfo.ToTitleCase($type)) $progName is missing."
}
return $false, $msg
}
Write-Host 'ok.' -f Green
return $true, $null
}
# for dealing with installers
function args($config, $dir, $global) {
if ($config) { return $config | ForEach-Object { (format $_ @{'dir' = $dir; 'global' = $global }) } }
@()
}
function run_installer($fname, $manifest, $architecture, $dir, $global) {
$installer = installer $manifest $architecture
if ($installer.script) {
Write-Output 'Running installer script...'
Invoke-Command ([scriptblock]::Create($installer.script -join "`r`n"))
return
}
if ($installer) {
$prog = "$dir\$(coalesce $installer.file "$fname")"
if (!(is_in_dir $dir $prog)) {
abort "Error in manifest: Installer $prog is outside the app directory."
$substitutions = @{
'$dir' = $Path
'$global' = $Global
'$version' = $Manifest.version
}
$arg = @(args $installer.args $dir $global)
if ($prog.endswith('.ps1')) {
& $prog @arg
$fnArgs = substitute $installer.args $substitutions
if ($progName.EndsWith('.ps1')) {
& $progName @fnArgs
} else {
$installed = Invoke-ExternalCommand $prog $arg -Activity 'Running installer...'
if (!$installed) {
abort "Installation aborted. You might need to run 'scoop uninstall $app' before trying again."
}
# Don't remove installer if "keep" flag is set to true
if (!($installer.keep -eq 'true')) {
Remove-Item $prog
}
}
}
}
function run_uninstaller($manifest, $architecture, $dir) {
$uninstaller = uninstaller $manifest $architecture
$version = $manifest.version
if ($uninstaller.script) {
Write-Output 'Running uninstaller script...'
Invoke-Command ([scriptblock]::Create($uninstaller.script -join "`r`n"))
return
}
if ($uninstaller.file) {
$prog = "$dir\$($uninstaller.file)"
$arg = args $uninstaller.args
if (!(is_in_dir $dir $prog)) {
warn "Error in manifest: Installer $prog is outside the app directory, skipping."
$prog = $null
} elseif (!(Test-Path $prog)) {
warn "Uninstaller $prog is missing, skipping."
$prog = $null
}
if ($prog) {
if ($prog.endswith('.ps1')) {
& $prog @arg
} else {
$uninstalled = Invoke-ExternalCommand $prog $arg -Activity 'Running uninstaller...'
if (!$uninstalled) {
$status = Invoke-ExternalCommand $progName -ArgumentList $fnArgs -Activity "Running $type ..."
if (!$status) {
if ($Uninstall) {
abort 'Uninstallation aborted.'
} else {
abort "Installation aborted. You might need to run 'scoop uninstall $AppName' before trying again."
}
}
# Don't remove installer if "keep" flag is set to true
if (!$installer.keep) {
Remove-Item $progName
}
}
}
Invoke-HookScript -HookType $type -Manifest $Manifest -ProcessorArchitecture $ProcessorArchitecture
}
function Invoke-HookScript {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('installer', 'pre_install', 'post_install', 'uninstaller', 'pre_uninstall', 'post_uninstall')]
[String] $HookType,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[PSCustomObject] $Manifest,
[Parameter(Mandatory = $true)]
[Alias('Arch', 'Architecture')]
[ValidateSet('32bit', '64bit', 'arm64')]
[string]
$ProcessorArchitecture
)
$script = arch_specific $HookType $Manifest $ProcessorArchitecture
if ($HookType -in @('installer', 'uninstaller')) {
$script = $script.script
}
if ($script) {
Write-Host "Running $HookType script..." -NoNewline
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
Write-Host 'done.' -ForegroundColor Green
}
}
# get target, name, arguments for shim
@@ -924,12 +332,12 @@ function env_rm_path($manifest, $dir, $global, $arch) {
}
}
function env_set($manifest, $dir, $global, $arch) {
function env_set($manifest, $global, $arch) {
$env_set = arch_specific 'env_set' $manifest $arch
if ($env_set) {
$env_set | Get-Member -Member NoteProperty | ForEach-Object {
$name = $_.name
$val = format $env_set.$($_.name) @{ 'dir' = $dir }
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
$name = $_.Name
$val = $ExecutionContext.InvokeCommand.ExpandString($env_set.$($name))
Set-EnvVar -Name $name -Value $val -Global:$global
Set-Content env:\$name $val
}
@@ -938,41 +346,20 @@ function env_set($manifest, $dir, $global, $arch) {
function env_rm($manifest, $global, $arch) {
$env_set = arch_specific 'env_set' $manifest $arch
if ($env_set) {
$env_set | Get-Member -Member NoteProperty | ForEach-Object {
$name = $_.name
$env_set | Get-Member -MemberType NoteProperty | ForEach-Object {
$name = $_.Name
Set-EnvVar -Name $name -Value $null -Global:$global
if (Test-Path env:\$name) { Remove-Item env:\$name }
}
}
}
function Invoke-HookScript {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('pre_install', 'post_install',
'pre_uninstall', 'post_uninstall')]
[String] $HookType,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[PSCustomObject] $Manifest,
[Parameter(Mandatory = $true)]
[ValidateSet('32bit', '64bit', 'arm64')]
[String] $Arch
)
$script = arch_specific $HookType $Manifest $Arch
if ($script) {
Write-Output "Running $HookType script..."
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
}
}
function show_notes($manifest, $dir, $original_dir, $persist_dir) {
if ($manifest.notes) {
Write-Output 'Notes'
Write-Output '-----'
Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }))
Write-Output '-----'
}
}
@@ -1033,7 +420,7 @@ function show_suggestions($suggested) {
}
if (!$fulfilled) {
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'."
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." -ForegroundColor DarkYellow
}
}
}

View File

@@ -110,13 +110,13 @@ function json_path([String] $json, [String] $jsonpath, [Hashtable] $substitution
# Return versions in reverse order
$result = [System.Linq.Enumerable]::Reverse($result)
}
if ($single) {
if ([System.Linq.Enumerable]::Count($result) -eq 1 -or $single) {
# Extract First value
$result = [System.Linq.Enumerable]::First($result)
# Convert first value to string
$result = $result.ToString()
} else {
$result = "$([String]::Join('\n', $result))"
$result = [Newtonsoft.Json.JsonConvert]::SerializeObject($result)
}
return $result
} catch [Exception] {

View File

@@ -23,7 +23,7 @@ function url_manifest($url) {
} catch {
throw
}
if(!$str) { return $null }
if (!$str) { return $null }
try {
$str | ConvertFrom-Json -ErrorAction Stop
} catch {
@@ -40,30 +40,74 @@ function Get-Manifest($app) {
$app = appname_from_url $url
$manifest = url_manifest $url
} else {
$app, $bucket, $version = parse_app $app
if ($bucket) {
$manifest = manifest $app $bucket
# Check if the manifest is already installed
if (installed $app) {
$global = installed $app $true
$ver = Select-CurrentVersion -AppName $app -Global:$global
if (!$ver) {
$app, $bucket, $ver = parse_app $app
$ver = Select-CurrentVersion -AppName $app -Global:$global
}
$install_info_path = "$(versiondir $app $ver $global)\install.json"
if (Test-Path $install_info_path) {
$install_info = parse_json $install_info_path
$bucket = $install_info.bucket
if (!$bucket) {
$url = $install_info.url
if ($url -match '^(ht|f)tps?://|\\\\') {
$manifest = url_manifest $url
}
if (!$manifest) {
if (Test-Path $url) {
$manifest = parse_json $url
} else {
# Fallback to installed manifest
$manifest = installed_manifest $app $ver $global
}
}
} else {
$manifest = manifest $app $bucket
if (!$manifest) {
$deprecated_dir = (Find-BucketDirectory -Name $bucket -Root) + '\deprecated'
$manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
}
}
}
} else {
foreach ($tekcub in Get-LocalBucket) {
$manifest = manifest $app $tekcub
if ($manifest) {
$bucket = $tekcub
break
$app, $bucket, $version = parse_app $app
if ($bucket) {
$manifest = manifest $app $bucket
} else {
$matched_buckets = @()
foreach ($tekcub in Get-LocalBucket) {
$current_manifest = manifest $app $tekcub
if (!$manifest -and $current_manifest) {
$manifest = $current_manifest
$bucket = $tekcub
}
if ($current_manifest) {
$matched_buckets += $tekcub
}
}
}
if (!$manifest) {
# couldn't find app in buckets: check if it's a local path
if (Test-Path $app) {
$url = Convert-Path $app
$app = appname_from_url $url
$manifest = parse_json $url
} else {
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
$app = appname_from_url $app
}
}
}
if (!$manifest) {
# couldn't find app in buckets: check if it's a local path
if (Test-Path $app) {
$url = Convert-Path $app
$app = appname_from_url $url
$manifest = url_manifest $url
} else {
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
$app = appname_from_url $app
}
}
}
if ($matched_buckets.Length -gt 1) {
warn "Multiple buckets contain manifest '$app', the current selection is '$bucket/$app'."
}
return $app, $manifest, $bucket, $url
}
@@ -128,33 +172,218 @@ function Get-SupportedArchitecture($manifest, $architecture) {
}
}
function Get-RelativePathCompat($from, $to) {
<#
.SYNOPSIS
Cross-platform compatible relative path function
.DESCRIPTION
Falls back to custom implementation for Windows PowerShell compatibility
#>
if ($PSVersionTable.PSVersion.Major -ge 6) {
# PowerShell Core/7+ - use built-in method
try {
return [System.IO.Path]::GetRelativePath($from, $to)
} catch {
# Fallback if method fails
}
}
# Windows PowerShell compatible implementation
$fromUri = New-Object System.Uri($from.TrimEnd('\') + '\')
$toUri = New-Object System.Uri($to)
if ($fromUri.Scheme -ne $toUri.Scheme) {
return $to # Cannot make relative path between different schemes
}
$relativeUri = $fromUri.MakeRelativeUri($toUri)
$relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())
return $relativePath -replace '/', '\'
}
function Find-HistoricalManifestInCache($app, $bucket, $requestedVersion) {
if (!(get_config USE_SQLITE_CACHE)) {
return $null
}
# Return null if bucket is null or empty
if (!$bucket) {
return $null
}
# Import database functions if not already loaded
if (!(Get-Command 'Get-ScoopDBItem' -ErrorAction SilentlyContinue)) {
. "$PSScriptRoot\database.ps1"
}
$dbResult = Get-ScoopDBItem -Name $app -Bucket $bucket -Version $requestedVersion
# Strictly follow DB contract: must be DataTable with at least one row
if (-not ($dbResult -is [System.Data.DataTable])) { return $null }
if ($dbResult.Rows.Count -eq 0) { return $null }
$manifestText = $dbResult.Rows[0]['manifest']
if ([string]::IsNullOrWhiteSpace($manifestText)) { return $null }
$manifestObj = $null
try { $manifestObj = $manifestText | ConvertFrom-Json -ErrorAction Stop } catch {}
$manifestVersion = if ($manifestObj -and $manifestObj.version) { $manifestObj.version } else { $requestedVersion }
return @{ ManifestText = $manifestText; version = $manifestVersion; source = "sqlite_exact_match" }
return $null
}
function Find-HistoricalManifestInGit($app, $bucket, $requestedVersion) {
# Only proceed if git history is enabled
if (!(get_config USE_GIT_HISTORY $true)) {
return $null
}
if (!$bucket) {
return $null
}
$bucketDir = Find-BucketDirectory $bucket -Root
if (!(Test-Path "$bucketDir\.git")) {
warn "Bucket '$bucket' is not a git repository. Cannot search historical versions."
return $null
}
$manifestPath = "$app.json"
$innerBucketDir = Find-BucketDirectory $bucket # Non-root path
if (-not (Test-Path $innerBucketDir -PathType Container)) {
warn "Could not find inner bucket directory for '$bucket' at '$innerBucketDir'."
return $null
}
$relativeManifestPath = Get-RelativePathCompat $bucketDir (Join-Path $innerBucketDir $manifestPath)
$relativeManifestPath = $relativeManifestPath -replace '\\', '/'
try {
# Prefer precise regex match on version line, fallback to -S literal
$pattern = '"version"\s*:\s*"' + [regex]::Escape($requestedVersion) + '"'
$commits = @()
$outG = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-G',$pattern,'--',$relativeManifestPath)
if ($outG) { $commits = @($outG | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
if ($commits.Count -eq 0) {
$searchLiteral = '"version": "' + $requestedVersion + '"'
$outS = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-S',$searchLiteral,'--',$relativeManifestPath)
if ($outS) { $commits = @($outS | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
}
if ($commits.Count -eq 0) { return $null }
$h = $commits[0]
# First try parent snapshot (latest state before change), then the change itself
foreach ($spec in @("$h^","$h")) {
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$spec`:$relativeManifestPath")
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
if ($content -is [Array]) { $content = $content -join "`n" }
try {
$obj = $content | ConvertFrom-Json -ErrorAction Stop
} catch { continue }
if ($obj -and $obj.version -eq $requestedVersion) {
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$spec" }
}
}
# Fallback: iterate recent commits that touched the version string and validate
$outAll = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','--format=%H','-G',$pattern,'--',$relativeManifestPath)
$allCommits = @()
if ($outAll) { $allCommits = @($outAll | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
foreach ($c in $allCommits) {
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$c`:$relativeManifestPath")
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
if ($content -is [Array]) { $content = $content -join "`n" }
try { $obj = $content | ConvertFrom-Json -ErrorAction Stop } catch { continue }
if ($obj -and $obj.version -eq $requestedVersion) {
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$c" }
}
}
return $null
} catch { return $null }
}
function Find-HistoricalManifest($app, $bucket, $version) {
# Orchestrates historical manifest lookup using available providers (DB → Git)
$result = $null
if (get_config USE_SQLITE_CACHE) {
$result = Find-HistoricalManifestInCache $app $bucket $version
if ($result) {
if ($result.ManifestText) {
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
return @{ path = $path; version = $result.version; source = $result.source }
}
return $result
}
}
if (get_config USE_GIT_HISTORY $true) {
$result = Find-HistoricalManifestInGit $app $bucket $version
if ($result) {
if ($result.ManifestText) {
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
return @{ path = $path; version = $result.version; source = $result.source }
}
return $result
}
}
return $null
}
function generate_user_manifest($app, $bucket, $version) {
# 'autoupdate.ps1' 'buckets.ps1' 'manifest.ps1'
$app, $manifest, $bucket, $null = Get-Manifest "$bucket/$app"
if ("$($manifest.version)" -eq "$version") {
return manifest_path $app $bucket
}
warn "Given version ($version) does not match manifest ($($manifest.version))"
warn "Attempting to generate manifest for '$app' ($version)"
# Try historical providers via orchestrator
$historicalResult = Find-HistoricalManifest $app $bucket $version
if ($historicalResult) { return $historicalResult.path }
# No historical manifest; try autoupdate if available
if (!($manifest.autoupdate)) {
abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'"
abort "Could not find manifest for '$app@$version' and no autoupdate is available"
}
ensure (usermanifestsdir) | out-null
ensure (usermanifestsdir) | Out-Null
$manifest_path = "$(usermanifestsdir)\$app.json"
try {
Invoke-AutoUpdate $app "$(Convert-Path (usermanifestsdir))\$app.json" $manifest $version $(@{ })
return Convert-Path (usermanifest $app)
Invoke-AutoUpdate $app $manifest_path $manifest $version $(@{ })
return $manifest_path
} catch {
write-host -f darkred "Could not install $app@$version"
warn "Autoupdate failed for '$app@$version'"
abort "Installation of '$app@$version' is not possible"
}
return $null
}
function url($manifest, $arch) { arch_specific 'url' $manifest $arch }
function installer($manifest, $arch) { arch_specific 'installer' $manifest $arch }
function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $arch }
function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch }
function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch}
function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch}
function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch }
function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch }
# Helper: write manifest text to user manifests cache directory and return path
function Write-ManifestToUserCache {
param(
[Parameter(Mandatory=$true, Position=0)][string]$App,
[Parameter(Mandatory=$true, Position=1)][string]$ManifestText
)
ensure (usermanifestsdir) | Out-Null
$tempManifestPath = "$(usermanifestsdir)\$App.json"
$ManifestText | Out-UTF8File -FilePath $tempManifestPath
return $tempManifestPath
}

View File

@@ -162,7 +162,7 @@ function env($name, $global, $val) {
function strip_path($orig_path, $dir) {
Show-DeprecatedWarning $MyInvocation 'Split-PathLikeEnvVar'
Split-PathLikeEnvVar -Name $dir -Path $orig_path
Split-PathLikeEnvVar -Pattern @($dir) -Path $orig_path
}
function add_first_in_path($dir, $global) {

View File

@@ -1,117 +1,68 @@
# Usage: scoop alias add|list|rm [<args>]
# Usage: scoop alias <subcommand> [options] [<args>]
# Summary: Manage scoop aliases
# Help: Add, remove or list Scoop aliases
# Help: Available subcommands: add, rm, list.
#
# Aliases are custom Scoop subcommands that can be created to make common tasks
# easier.
# Aliases are custom Scoop subcommands that can be created to make common tasks easier.
#
# To add an Alias:
# scoop alias add <name> <command> <description>
# To add an alias:
#
# e.g.:
# scoop alias add rm 'scoop uninstall $args[0]' 'Uninstalls an app'
# scoop alias add upgrade 'scoop update *' 'Updates all apps, just like brew or apt'
# scoop alias add <name> <command> [<description>]
#
# e.g.,
#
# scoop alias add rm 'scoop uninstall $args[0]' 'Uninstall an app'
# scoop alias add upgrade 'scoop update *' 'Update all apps, just like "brew" or "apt"'
#
# To remove an alias:
#
# scoop alias rm <name>
#
# To list all aliases:
#
# scoop alias list [-v|--verbose]
#
# Options:
# -v, --verbose Show alias description and table headers (works only for 'list')
# -v, --verbose Show alias description and table headers (works only for "list")
param(
[String]$opt,
[String]$name,
[String]$command,
[String]$description,
[Switch]$verbose = $false
)
param($SubCommand)
. "$PSScriptRoot\..\lib\install.ps1" # shim related
. "$PSScriptRoot\..\lib\getopt.ps1"
$script:config_alias = 'alias'
function init_alias_config {
$aliases = get_config $script:config_alias
if ($aliases) {
$aliases
$SubCommands = @('add', 'rm', 'list')
if ($SubCommand -notin $SubCommands) {
if (!$SubCommand) {
error '<subcommand> missing'
} else {
New-Object -TypeName PSObject
error "'$SubCommand' is not one of available subcommands: $($SubCommands -join ', ')"
}
my_usage
exit 1
}
function add_alias($name, $command) {
if (!$command) {
abort "Can't create an empty alias."
$opt, $other, $err = getopt $Args 'v' 'verbose'
if ($err) { "scoop alias: $err"; exit 1 }
$name, $command, $description = $other
$verbose = $opt.v -or $opt.verbose
switch ($SubCommand) {
'add' {
if (!$name -or !$command) {
error "<name> and <command> must be specified for subcommand 'add'"
exit 1
}
add_alias $name $command $description
}
# get current aliases from config
$aliases = init_alias_config
if ($aliases.$name) {
abort "Alias '$name' already exists."
'rm' {
if (!$name) {
error "<name> must be specified for subcommand 'rm'"
exit 1
}
rm_alias $name
}
$alias_file = "scoop-$name"
# generate script
$shimdir = shimdir $false
if (Test-Path "$shimdir\$alias_file.ps1") {
abort "File '$alias_file.ps1' already exists in shims directory."
'list' {
list_aliases $verbose
}
$script =
@(
"# Summary: $description",
"$command"
) -join "`r`n"
$script | Out-UTF8File "$shimdir\$alias_file.ps1"
# add alias to config
$aliases | Add-Member -MemberType NoteProperty -Name $name -Value $alias_file
set_config $script:config_alias $aliases | Out-Null
}
function rm_alias($name) {
$aliases = init_alias_config
if (!$name) {
abort 'Alias to be removed has not been specified!'
}
if ($aliases.$name) {
info "Removing alias '$name'..."
rm_shim $aliases.$name (shimdir $false)
$aliases.PSObject.Properties.Remove($name)
set_config $script:config_alias $aliases | Out-Null
} else {
abort "Alias '$name' doesn't exist."
}
}
function list_aliases {
$aliases = @()
(init_alias_config).PSObject.Properties.GetEnumerator() | ForEach-Object {
$content = Get-Content (command_path $_.Name)
$command = ($content | Select-Object -Skip 1).Trim()
$summary = (summary $content).Trim()
$aliases += New-Object psobject -Property @{Name = $_.name; Summary = $summary; Command = $command }
}
if (!$aliases.count) {
info "No alias found."
}
$aliases = $aliases.GetEnumerator() | Sort-Object Name
if ($verbose) {
return $aliases | Select-Object Name, Command, Summary
} else {
return $aliases | Select-Object Name, Command
}
}
switch ($opt) {
'add' { add_alias $name $command }
'rm' { rm_alias $name }
'list' { list_aliases }
default { my_usage; exit 1 }
}
exit 0

View File

@@ -19,6 +19,15 @@
# scoop bucket known
param($cmd, $name, $repo)
if (get_config NO_JUNCTION) {
. "$PSScriptRoot\..\lib\versions.ps1"
}
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\database.ps1"
}
$usage_add = 'usage: scoop bucket add <name> [<repo>]'
$usage_rm = 'usage: scoop bucket rm <name>'

View File

@@ -16,7 +16,7 @@ param($cmd)
function cacheinfo($file) {
$app, $version, $url = $file.Name -split '#'
New-Object PSObject -Property @{ Name = $app; Version = $version; Length = $file.Length; URL = $url }
New-Object PSObject -Property @{ Name = $app; Version = $version; Length = $file.Length }
}
function cacheshow($app) {
@@ -28,7 +28,7 @@ function cacheshow($app) {
$files = @(Get-ChildItem $cachedir | Where-Object -Property Name -Value "^$app#" -Match)
$totalLength = ($files | Measure-Object -Property Length -Sum).Sum
$files | ForEach-Object { cacheinfo $_ } | Select-Object Name, Version, Length, URL
$files | ForEach-Object { cacheinfo $_ } | Select-Object Name, Version, Length
Write-Host "Total: $($files.Length) $(pluralize $files.Length 'file' 'files'), $(filesize $totalLength)" -ForegroundColor Yellow
}
@@ -48,7 +48,7 @@ function cacheremove($app) {
$files | ForEach-Object {
$curr = cacheinfo $_
Write-Host "Removing $($curr.URL)..."
Write-Host "Removing $($_.Name)..."
Remove-Item $_.FullName
if(Test-Path "$cachedir\$($curr.Name).txt") {
Remove-Item "$cachedir\$($curr.Name).txt"

View File

@@ -7,7 +7,9 @@
param($app)
. "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
if (!$app) { error '<app> missing'; my_usage; exit 1 }

View File

@@ -27,6 +27,14 @@
# use_lessmsi: $true|$false
# Prefer lessmsi utility over native msiexec.
#
# use_sqlite_cache: $true|$false
# Use SQLite database for caching. This is useful for speeding up 'scoop search' and 'scoop shim' commands.
#
# use_git_history: $true|$false
# Enable searching for specific versions in git history when installing apps with version specifiers.
# When enabled, Scoop will first search the bucket's git history for the exact version before falling back to autoupdate.
# (Default is $true)
#
# no_junction: $true|$false
# The 'current' version alias will not be used. Shims and shortcuts will point to specific version instead.
#
@@ -163,7 +171,7 @@ if (!$name) {
Write-Host "'$name' has been set to '$value'"
} else {
$value = get_config $name
if($null -eq $value) {
if ($null -eq $value) {
Write-Host "'$name' is not set"
} else {
if ($value -is [System.DateTime]) {

View File

@@ -3,7 +3,9 @@
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' (indirectly)
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
$opt, $apps, $err = getopt $args 'a:' 'arch='
$app = $apps[0]
@@ -20,7 +22,14 @@ try {
$deps = @()
Get-Dependency $app $architecture | ForEach-Object {
$dep = [ordered]@{}
$dep.Source, $dep.Name = $_ -split '/'
$app, $null, $bucket, $url = Get-Manifest $_
if (!$url) {
$bucket, $app = $_ -split '/'
}
$dep.Source = if ($url) { $url } else { $bucket }
$dep.Name = $app
$deps += [PSCustomObject]$dep
}
$deps

View File

@@ -15,20 +15,25 @@
#
# Options:
# -f, --force Force download (overwrite cache)
# -h, --no-hash-check Skip hash verification (use with caution!)
# -s, --skip-hash-check Skip hash verification (use with caution!)
# -u, --no-update-scoop Don't update Scoop before downloading if it's outdated
# -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly)
. "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
. "$PSScriptRoot\..\lib\install.ps1" # 'nightly_version'
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}
$opt, $apps, $err = getopt $args 'fhua:' 'force', 'no-hash-check', 'no-update-scoop', 'arch='
$opt, $apps, $err = getopt $args 'fsua:' 'force', 'skip-hash-check', 'no-update-scoop', 'arch='
if ($err) { error "scoop download: $err"; exit 1 }
$check_hash = !($opt.h -or $opt.'no-hash-check')
$check_hash = !($opt.s -or $opt.'skip-hash-check')
$use_cache = !($opt.f -or $opt.force)
$architecture = Get-DefaultArchitecture
try {

View File

@@ -29,14 +29,13 @@ if ($global -and !(is_admin)) {
exit 1
}
$apps | ForEach-Object {
$app = $_
foreach ($app in $apps) {
if ($app -eq 'scoop') {
$hold_update_until = [System.DateTime]::Now.AddDays(1)
set_config HOLD_UPDATE_UNTIL $hold_update_until.ToString('o') | Out-Null
success "$app is now held and might not be updated until $($hold_update_until.ToLocalTime())."
return
continue
}
if (!(installed $app $global)) {
if ($global) {
@@ -44,7 +43,7 @@ $apps | ForEach-Object {
} else {
error "'$app' is not installed."
}
return
continue
}
if (get_config NO_JUNCTION) {

View File

@@ -2,7 +2,9 @@
# Summary: Opens the app homepage
param($app)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
if ($app) {
$null, $manifest, $bucket, $null = Get-Manifest $app

View File

@@ -5,9 +5,11 @@
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion', 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-RemoteFileSize'
$opt, $app, $err = getopt $args 'v' 'verbose'
$original_app = $app
if ($err) { error "scoop info: $err"; exit 1 }
$verbose = $opt.v -or $opt.verbose
@@ -22,7 +24,7 @@ if (!$manifest) {
$global = installed $app $true
$status = app_status $app $global
$install = install_info $app $status.version $global
$status.installed = $bucket -and $install.bucket -eq $bucket
$status.installed = ($bucket -and $install.bucket -eq $bucket) -or (installed $app)
$version_output = $manifest.version
$manifest_file = if ($bucket) {
manifest_path $app $bucket
@@ -30,12 +32,30 @@ $manifest_file = if ($bucket) {
$url
}
# Standalone and Source detection
if ((Test-Path $original_app) -or ($original_app -match '^(ht|f)tps?://|\\\\')) {
$standalone = $true
if (Test-Path $original_app) {
$original_app = (Get-AbsolutePath "$original_app")
}
if ($install.url) {
if (Test-Path $install.url) {
$install_url = (Get-AbsolutePath $install.url)
} else {
$install_url = $install.url
}
}
if ($original_app -eq $install_url) {
$same_source = $true
}
}
if ($verbose) {
$dir = currentdir $app $global
$original_dir = versiondir $app $manifest.version $global
$persist_dir = persistdir $app $global
} else {
$dir, $original_dir, $persist_dir = "<root>", "<root>", "<root>"
$dir, $original_dir, $persist_dir = '<root>', '<root>', '<root>'
}
if ($status.installed) {
@@ -43,21 +63,39 @@ if ($status.installed) {
if ($install.url) {
$manifest_file = $install.url
}
if ($status.version -eq $manifest.version) {
if ($status.deprecated) {
$manifest_file = $status.deprecated
} elseif ($standalone -and !$same_source) {
$version_output = $manifest.version
} elseif ($status.version -eq $manifest.version) {
$version_output = $status.version
} else {
$version_output = "$($status.version) (Update to $($manifest.version) available)"
}
}
$item = [ordered]@{ Name = $app }
if ($status.deprecated) {
$item.Name += ' (DEPRECATED)'
}
if ($manifest.description) {
$item.Description = $manifest.description
}
$item.Version = $version_output
if ($bucket) {
$item.Bucket = $bucket
$item.Source = if ($standalone) {
$original_app
} else {
if ($install.bucket) {
$install.bucket
} elseif ($install.url) {
$install.url
} else {
$bucket
}
}
if ($manifest.homepage) {
$item.Website = $manifest.homepage.TrimEnd('/')
}
@@ -69,7 +107,7 @@ if ($manifest.license) {
$manifest.license
} elseif ($manifest.license -match '[|,]') {
if ($verbose) {
"$($manifest.license) ($(($manifest.license -Split "\||," | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
"$($manifest.license) ($(($manifest.license -Split '\||,' | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
} else {
$manifest.license
}
@@ -84,7 +122,7 @@ if ($manifest.depends) {
if (Test-Path $manifest_file) {
if (Get-Command git -ErrorAction Ignore) {
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#'
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aI#%an', $manifest_file) 2> $null) -Split '#'
}
if ($gitinfo) {
$item.'Updated at' = $gitinfo[0] | Get-Date
@@ -100,11 +138,13 @@ if ($verbose) { $item.Manifest = $manifest_file }
if ($status.installed) {
# Show installed versions
$installed_output = @()
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { " *global*" })" }
if (!$standalone -or $same_source) {
$installed_output = @()
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { ' *global*' })" }
}
$item.Installed = $installed_output -join "`n"
}
$item.Installed = $installed_output -join "`n"
if ($verbose) {
# Show size of installation
@@ -161,12 +201,12 @@ if ($status.installed) {
foreach ($url in @(url $manifest (Get-DefaultArchitecture))) {
try {
if (Test-Path (cache_path $app $manifest.version $url)) {
$cached = " (latest version is cached)"
$cached = ' (latest version is cached)'
} else {
$cached = $null
}
[int]$urlLength = (Invoke-WebRequest $url -Method Head).Headers.'Content-Length'[0]
$urlLength = Get-RemoteFileSize $url
$totalPackage += $urlLength
} catch [System.Management.Automation.RuntimeException] {
$totalPackage = 0
@@ -196,7 +236,7 @@ if ($binaries) {
$binary_output += $_
}
}
$item.Binaries = $binary_output -join " | "
$item.Binaries = $binary_output -join ' | '
}
$shortcuts = @(arch_specific 'shortcuts' $manifest $install.architecture)
if ($shortcuts) {
@@ -204,13 +244,13 @@ if ($shortcuts) {
$shortcuts | ForEach-Object {
$shortcut_output += $_[1]
}
$item.Shortcuts = $shortcut_output -join " | "
$item.Shortcuts = $shortcut_output -join ' | '
}
$env_set = arch_specific 'env_set' $manifest $install.architecture
if ($env_set) {
$env_vars = @()
$env_set | Get-Member -member noteproperty | ForEach-Object {
$env_vars += "$($_.name) = $(format $env_set.$($_.name) @{ "dir" = $dir })"
$env_vars += "$($_.name) = $(substitute $env_set.$($_.name) @{ '$dir' = $dir })"
}
$item.Environment = $env_vars -join "`n"
}

View File

@@ -4,21 +4,27 @@
# scoop install git
#
# To install a different version of the app
# (note that this will auto-generate the manifest using current version):
# (will search sqlite cache if enabled, then git history (if use_git_history is $true), then auto-generate manifest as fallback):
# scoop install gh@2.7.0
#
# To install an app from a manifest at a URL:
# scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/runat.json
#
# To install a different version of the app from a URL:
# scoop install https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/neovim.json@0.9.0
#
# To install an app from a manifest on your computer
# scoop install \path\to\app.json
#
# To install an app from a manifest on your computer
# scoop install \path\to\app.json@version
#
# Options:
# -g, --global Install the app globally
# -i, --independent Don't install dependencies automatically
# -k, --no-cache Don't use the download cache
# -s, --skip-hash-check Skip hash validation (use with caution!)
# -u, --no-update-scoop Don't update Scoop before installing if it's outdated
# -s, --skip Skip hash validation (use with caution!)
# -a, --arch <32bit|64bit|arm64> Use the specified architecture, if the app supports it
. "$PSScriptRoot\..\lib\getopt.ps1"
@@ -27,17 +33,21 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\system.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
. "$PSScriptRoot\..\lib\decompress.ps1"
. "$PSScriptRoot\..\lib\shortcuts.ps1"
. "$PSScriptRoot\..\lib\psmodules.ps1"
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\depends.ps1"
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}
$opt, $apps, $err = getopt $args 'gikusa:' 'global', 'independent', 'no-cache', 'no-update-scoop', 'skip', 'arch='
$opt, $apps, $err = getopt $args 'giksua:' 'global', 'independent', 'no-cache', 'skip-hash-check', 'no-update-scoop', 'arch='
if ($err) { "scoop install: $err"; exit 1 }
$global = $opt.g -or $opt.global
$check_hash = !($opt.s -or $opt.skip)
$check_hash = !($opt.s -or $opt.'skip-hash-check')
$independent = $opt.i -or $opt.independent
$use_cache = !($opt.k -or $opt.'no-cache')
$architecture = Get-DefaultArchitecture
@@ -82,7 +92,7 @@ $specific_versions = $apps | Where-Object {
}
# compare object does not like nulls
if ($specific_versions.length -gt 0) {
if ($specific_versions.Count -gt 0) {
$difference = Compare-Object -ReferenceObject $apps -DifferenceObject $specific_versions -PassThru
} else {
$difference = $apps
@@ -91,13 +101,13 @@ if ($specific_versions.length -gt 0) {
$specific_versions_paths = $specific_versions | ForEach-Object {
$app, $bucket, $version = parse_app $_
if (installed_manifest $app $version) {
warn "'$app' ($version) is already installed.`nUse 'scoop update $app$(if ($global) { " --global" })' to install a new version."
warn "'$app' ($version) is already installed.`nUse 'scoop update $app$(if ($global) { ' --global' })' to install a new version."
continue
}
generate_user_manifest $app $bucket $version
}
$apps = @(($specific_versions_paths + $difference) | Where-Object { $_ } | Sort-Object -Unique)
$apps = @((@($specific_versions_paths) + $difference) | Where-Object { $_ } | Select-Object -Unique)
# remember which were explictly requested so that we can
# differentiate after dependencies are added

View File

@@ -5,8 +5,9 @@ param($query)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'parse_json' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
$def_arch = Get-DefaultArchitecture
$defaultArchitecture = Get-DefaultArchitecture
if (-not (Get-FormatData ScoopApps)) {
Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml"
}
@@ -47,10 +48,11 @@ $apps | Where-Object { !$query -or ($_.name -match $query) } | ForEach-Object {
$item.Updated = $updated
$info = @()
if ((app_status $app $global).deprecated) { $info += 'Deprecated package'}
if ($global) { $info += 'Global install' }
if (failed $app $global) { $info += 'Install failed' }
if ($install_info.hold) { $info += 'Held package' }
if ($install_info.architecture -and $def_arch -ne $install_info.architecture) {
if ($install_info.architecture -and $defaultArchitecture -ne $install_info.architecture) {
$info += $install_info.architecture
}
$item.Info = $info -join ', '

View File

@@ -84,7 +84,7 @@ $apps | ForEach-Object {
env_rm_path $manifest $dir $global $architecture
env_rm $manifest $global $architecture
env_add_path $manifest $dir $global $architecture
env_set $manifest $dir $global $architecture
env_set $manifest $global $architecture
# unlink all potential old link before re-persisting
unlink_persist_data $manifest $original_dir
persist_data $manifest $original_dir $persist_dir

View File

@@ -3,26 +3,17 @@
# Help: Searches for apps that are available to install.
#
# If used with [query], shows app names that match the query.
# - With 'use_sqlite_cache' enabled, [query] is partially matched against app names, binaries, and shortcuts.
# - Without 'use_sqlite_cache', [query] can be a regular expression to match against app names and binaries.
# Without [query], shows all the available apps.
param($query)
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion'
. "$PSScriptRoot\..\lib\download.ps1"
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
$query = New-Object Regex $query, 'IgnoreCase'
} catch {
abort "Invalid regular expression: $($_.Exception.InnerException.Message)"
}
$githubtoken = Get-GitHubToken
$authheader = @{}
if ($githubtoken) {
$authheader = @{'Authorization' = "token $githubtoken"}
}
function bin_match($manifest, $query) {
if (!$manifest.bin) { return $false }
$bins = foreach ($bin in $manifest.bin) {
@@ -39,16 +30,16 @@ function bin_match($manifest, $query) {
function bin_match_json($json, $query) {
[System.Text.Json.JsonElement]$bin = [System.Text.Json.JsonElement]::new()
if (!$json.RootElement.TryGetProperty("bin", [ref] $bin)) { return $false }
if (!$json.RootElement.TryGetProperty('bin', [ref] $bin)) { return $false }
$bins = @()
if($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) {
if ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) {
$bins += [System.IO.Path]::GetFileName($bin)
} elseif ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
foreach($subbin in $bin.EnumerateArray()) {
if($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) {
foreach ($subbin in $bin.EnumerateArray()) {
if ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) {
$bins += [System.IO.Path]::GetFileName($subbin)
} elseif ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
if([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) {
if ([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) {
$bins += [System.IO.Path]::GetFileName($subbin[0])
} elseif ($subbin.GetArrayLength() -ge 2 -and $subbin[1] -match $query) {
$bins += $subbin[1]
@@ -65,25 +56,33 @@ function search_bucket($bucket, $query) {
$apps = Get-ChildItem (Find-BucketDirectory $bucket) -Filter '*.json' -Recurse
$apps | ForEach-Object {
$json = [System.Text.Json.JsonDocument]::Parse([System.IO.File]::ReadAllText($_.FullName))
$filepath = $_.FullName
$json = try {
[System.Text.Json.JsonDocument]::Parse([System.IO.File]::ReadAllText($filepath))
} catch {
debug "Failed to parse manifest file: $filepath (error: $_)"
return
}
$name = $_.BaseName
if ($name -match $query) {
$list.Add([PSCustomObject]@{
Name = $name
Version = $json.RootElement.GetProperty("version")
Source = $bucket
Binaries = ""
})
Name = $name
Version = $json.RootElement.GetProperty('version')
Source = $bucket
Binaries = ''
})
} else {
$bin = bin_match_json $json $query
if ($bin) {
$list.Add([PSCustomObject]@{
Name = $name
Version = $json.RootElement.GetProperty("version")
Source = $bucket
Binaries = $bin -join ' | '
})
Name = $name
Version = $json.RootElement.GetProperty('version')
Source = $bucket
Binaries = $bin -join ' | '
})
}
}
}
@@ -99,42 +98,25 @@ function search_bucket_legacy($bucket, $query) {
if ($name -match $query) {
$list.Add([PSCustomObject]@{
Name = $name
Version = $manifest.Version
Source = $bucket
Binaries = ""
})
Name = $name
Version = $manifest.Version
Source = $bucket
Binaries = ''
})
} else {
$bin = bin_match $manifest $query
if ($bin) {
$list.Add([PSCustomObject]@{
Name = $name
Version = $manifest.Version
Source = $bucket
Binaries = $bin -join ' | '
})
Name = $name
Version = $manifest.Version
Source = $bucket
Binaries = $bin -join ' | '
})
}
}
}
}
function download_json($url) {
$ProgressPreference = 'SilentlyContinue'
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
$ProgressPreference = 'Continue'
$result
}
function github_ratelimit_reached {
$api_link = 'https://api.github.com/rate_limit'
$ret = (download_json $api_link).rate.remaining -eq 0
if ($ret) {
Write-Host "GitHub API rate limit reached.
Please try again later or configure your API token using 'scoop config gh_token <your token>'."
}
$ret
}
function search_remote($bucket, $query) {
$uri = [System.Uri](known_bucket_repo $bucket)
if ($uri.AbsolutePath -match '/([a-zA-Z0-9]*)/([a-zA-Z0-9-]*)(?:.git|/)?') {
@@ -154,7 +136,7 @@ function search_remotes($query) {
$names = $buckets | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
$results = $names | Where-Object { !(Test-Path $(Find-BucketDirectory $_)) } | ForEach-Object {
@{ "bucket" = $_; "results" = (search_remote $_ $query) }
@{ 'bucket' = $_; 'results' = (search_remote $_ $query) }
} | Where-Object { $_.results }
if ($results.count -gt 0) {
@@ -175,25 +157,45 @@ function search_remotes($query) {
$remote_list
}
$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq "System.Text.Json" }
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
Select-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
Select-Object -Property name, version, bucket, binary |
ForEach-Object {
$list.Add([PSCustomObject]@{
Name = $_.name
Version = $_.version
Source = $_.bucket
Binaries = $_.binary
})
}
} else {
try {
$query = New-Object Regex $query, 'IgnoreCase'
} catch {
abort "Invalid regular expression: $($_.Exception.InnerException.Message)"
}
Get-LocalBucket | ForEach-Object {
if ($jsonTextAvailable) {
search_bucket $_ $query
} else {
search_bucket_legacy $_ $query
$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq 'System.Text.Json' }
Get-LocalBucket | ForEach-Object {
if ($jsonTextAvailable) {
search_bucket $_ $query
} else {
search_bucket_legacy $_ $query
}
}
}
if ($list.Count -gt 0) {
Write-Host "Results from local buckets..."
Write-Host 'Results from local buckets...'
$list
}
if ($list.Count -eq 0 -and !(github_ratelimit_reached)) {
$remote_results = search_remotes $query
if (!$remote_results) {
warn "No matches found."
warn 'No matches found.'
exit 1
}
$remote_results

View File

@@ -6,6 +6,7 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest' 'parse_json' "install_info"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
# check if scoop needs updating
$currentdir = versiondir 'scoop' 'current'
@@ -58,7 +59,7 @@ $true, $false | ForEach-Object { # local and global apps
Get-ChildItem $dir | Where-Object name -NE 'scoop' | ForEach-Object {
$app = $_.name
$status = app_status $app $global
if (!$status.outdated -and !$status.failed -and !$status.removed -and !$status.missing_deps) { return }
if (!$status.outdated -and !$status.failed -and !$status.deprecated -and !$status.removed -and !$status.missing_deps) { return }
$item = [ordered]@{}
$item.Name = $app
@@ -66,9 +67,10 @@ $true, $false | ForEach-Object { # local and global apps
$item.'Latest Version' = if ($status.outdated) { $status.latest_version } else { "" }
$item.'Missing Dependencies' = $status.missing_deps -Split ' ' -Join ' | '
$info = @()
if ($status.failed) { $info += 'Install failed' }
if ($status.hold) { $info += 'Held package' }
if ($status.removed) { $info += 'Manifest removed' }
if ($status.failed) { $info += 'Install failed' }
if ($status.hold) { $info += 'Held package' }
if ($status.deprecated) { $info += 'Deprecated' }
if ($status.removed) { $info += 'Manifest removed' }
$item.Info = $info -join ', '
$list += [PSCustomObject]$item
}

View File

@@ -58,6 +58,7 @@ if (!$apps) { exit 0 }
$manifest = installed_manifest $app $version $global
$install = install_info $app $version $global
$architecture = $install.architecture
$bucket = $install.bucket
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture
@@ -74,7 +75,7 @@ if (!$apps) { exit 0 }
continue
}
run_uninstaller $manifest $architecture $dir
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global $global -Uninstall
rm_shims $app $manifest $global $architecture
rm_startmenu_shortcuts $manifest $global $architecture

View File

@@ -6,13 +6,13 @@
# You can use '*' in place of <app> to update all apps.
#
# Options:
# -f, --force Force update even when there isn't a newer version
# -g, --global Update a globally installed app
# -i, --independent Don't install dependencies automatically
# -k, --no-cache Don't use the download cache
# -s, --skip Skip hash validation (use with caution!)
# -q, --quiet Hide extraneous messages
# -a, --all Update all apps (alternative to '*')
# -f, --force Force update even when there isn't a newer version
# -g, --global Update a globally installed app
# -i, --independent Don't install dependencies automatically
# -k, --no-cache Don't use the download cache
# -s, --skip-hash-check Skip hash validation (use with caution!)
# -q, --quiet Hide extraneous messages
# -a, --all Update all apps (alternative to '*')
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\json.ps1" # 'save_install_info' in 'manifest.ps1' (indirectly)
@@ -24,12 +24,16 @@
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\depends.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}
$opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip', 'quiet', 'all'
$opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip-hash-check', 'quiet', 'all'
if ($err) { "scoop update: $err"; exit 1 }
$global = $opt.g -or $opt.global
$force = $opt.f -or $opt.force
$check_hash = !($opt.s -or $opt.skip)
$check_hash = !($opt.s -or $opt.'skip-hash-check')
$use_cache = !($opt.k -or $opt.'no-cache')
$quiet = $opt.q -or $opt.quiet
$independent = $opt.i -or $opt.independent
@@ -38,21 +42,21 @@ $all = $opt.a -or $opt.all
# load config
$configRepo = get_config SCOOP_REPO
if (!$configRepo) {
$configRepo = "https://github.com/ScoopInstaller/Scoop"
$configRepo = 'https://github.com/ScoopInstaller/Scoop'
set_config SCOOP_REPO $configRepo | Out-Null
}
# Find current update channel from config
$configBranch = get_config SCOOP_BRANCH
if (!$configBranch) {
$configBranch = "master"
$configBranch = 'master'
set_config SCOOP_BRANCH $configBranch | Out-Null
}
if(($PSVersionTable.PSVersion.Major) -lt 5) {
if (($PSVersionTable.PSVersion.Major) -lt 5) {
# check powershell version
Write-Output "PowerShell 5 or later is required to run Scoop."
Write-Output "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows"
Write-Output 'PowerShell 5 or later is required to run Scoop.'
Write-Output 'Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows'
break
}
$show_update_log = get_config SHOW_UPDATE_LOG $true
@@ -63,14 +67,14 @@ function Sync-Scoop {
[Switch]$Log
)
# Test if Scoop Core is hold
if(Test-ScoopCoreOnHold) {
if (Test-ScoopCoreOnHold) {
return
}
# check for git
if (!(Test-GitAvailable)) { abort "Scoop uses Git to update itself. Run 'scoop install git' and try again." }
Write-Host "Updating Scoop..."
Write-Host 'Updating Scoop...'
$currentdir = versiondir 'scoop' 'current'
if (!(Test-Path "$currentdir\.git")) {
$newdir = "$currentdir\..\new"
@@ -108,10 +112,10 @@ function Sync-Scoop {
# Stash uncommitted changes
if (Invoke-Git -Path $currentdir -ArgumentList @('diff', 'HEAD', '--name-only')) {
if (get_config AUTOSTASH_ON_CONFLICT) {
warn "Uncommitted changes detected. Stashing..."
warn 'Uncommitted changes detected. Stashing...'
Invoke-Git -Path $currentdir -ArgumentList @('stash', 'push', '-m', "WIP at $([System.DateTime]::Now.ToString('o'))", '-u', '-q')
} else {
warn "Uncommitted changes detected. Update aborted."
warn 'Uncommitted changes detected. Update aborted.'
return
}
}
@@ -132,7 +136,7 @@ function Sync-Scoop {
# reset branch HEAD
Invoke-Git -Path $currentdir -ArgumentList @('reset', '--hard', "origin/$configBranch", '-q')
} else {
Invoke-Git -Path $currentdir -ArgumentList @('pull', '-q')
Invoke-Git -Path $currentdir -ArgumentList @('pull', '--tags', '--force', '-q')
}
$res = $lastexitcode
@@ -152,7 +156,7 @@ function Sync-Bucket {
Param (
[Switch]$Log
)
Write-Host "Updating Buckets..."
Write-Host 'Updating Buckets...'
if (!(Test-Path (Join-Path (Find-BucketDirectory 'main' -Root) '.git'))) {
info "Converting 'main' bucket to git repo..."
@@ -170,40 +174,88 @@ function Sync-Bucket {
$buckets = Get-LocalBucket | ForEach-Object {
$path = Find-BucketDirectory $_ -Root
return @{
name = $_
name = $_
valid = Test-Path (Join-Path $path '.git')
path = $path
path = $path
}
}
$buckets | Where-Object { !$_.valid } | ForEach-Object { Write-Host "'$($_.name)' is not a git repository. Skipped." }
$updatedFiles = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
$removedFiles = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new())
if ($PSVersionTable.PSVersion.Major -ge 7) {
# Parallel parameter is available since PowerShell 7
$buckets | Where-Object { $_.valid } | ForEach-Object -ThrottleLimit 5 -Parallel {
. "$using:PSScriptRoot\..\lib\core.ps1"
. "$using:PSScriptRoot\..\lib\buckets.ps1"
$bucketLoc = $_.path
$name = $_.name
$bucketLoc = $_.path
$innerBucketLoc = Find-BucketDirectory $name
$previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD')
Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q')
if ($using:Log) {
Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit
}
if (get_config USE_SQLITE_CACHE) {
Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-status', $previousCommit) | ForEach-Object {
$status, $file = $_ -split '\s+', 2
$filePath = Join-Path $bucketLoc $file
if ($filePath -match "^$([regex]::Escape($innerBucketLoc)).*\.json$") {
switch ($status) {
{ $_ -in 'A', 'M', 'R' } {
[void]($using:updatedFiles).Add($filePath)
}
'D' {
[void]($using:removedFiles).Add([pscustomobject]@{
Name = ([System.IO.FileInfo]$file).BaseName
Bucket = $name
})
}
}
}
}
}
}
} else {
$buckets | Where-Object { $_.valid } | ForEach-Object {
$bucketLoc = $_.path
$name = $_.name
$bucketLoc = $_.path
$innerBucketLoc = Find-BucketDirectory $name
$previousCommit = Invoke-Git -Path $bucketLoc -ArgumentList @('rev-parse', 'HEAD')
Invoke-Git -Path $bucketLoc -ArgumentList @('pull', '-q')
if ($Log) {
Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit
}
if (get_config USE_SQLITE_CACHE) {
Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-status', $previousCommit) | ForEach-Object {
$status, $file = $_ -split '\s+', 2
$filePath = Join-Path $bucketLoc $file
if ($filePath -match "^$([regex]::Escape($innerBucketLoc)).*\.json$") {
switch ($status) {
{ $_ -in 'A', 'M', 'R' } {
[void]($updatedFiles).Add($filePath)
}
'D' {
[void]($removedFiles).Add([pscustomobject]@{
Name = ([System.IO.FileInfo]$file).BaseName
Bucket = $name
})
}
}
}
}
}
}
}
if ((get_config USE_SQLITE_CACHE) -and ($updatedFiles.Count -gt 0 -or $removedFiles.Count -gt 0)) {
info 'Updating cache...'
Set-ScoopDB -Path $updatedFiles
$removedFiles | Remove-ScoopDBItem
}
}
function update($app, $global, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) {
@@ -251,7 +303,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
# region Workaround
# Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored
# Remove and replace whole region after proper fix
Write-Host "Downloading new version"
Write-Host 'Downloading new version'
if (Test-Aria2Enabled) {
Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash
} else {
@@ -269,12 +321,12 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
error $err
if (Test-Path $source) {
# rm cached file
Remove-Item -force $source
Remove-Item -Force $source
}
if ($url.Contains('sourceforge.net')) {
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
}
abort $(new_issue_msg $app $bucket "hash check failed")
abort $(new_issue_msg $app $bucket 'hash check failed')
}
}
}
@@ -289,7 +341,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $old_manifest -Arch $architecture
Write-Host "Uninstalling '$app' ($old_version)"
run_uninstaller $old_manifest $architecture $dir
Invoke-Installer -Path $dir -Manifest $old_manifest -ProcessorArchitecture $architecture -Uninstall
rm_shims $app $old_manifest $global $architecture
# If a junction was used during install, that will have been used
@@ -404,11 +456,11 @@ if (-not ($apps -or $all)) {
} elseif ($outdated.Length -eq 0) {
Write-Host -f Green "Latest versions for all apps are installed! For more information try 'scoop status'"
} else {
Write-Host -f DarkCyan "Updating one outdated app:"
Write-Host -f DarkCyan 'Updating one outdated app:'
}
}
$suggested = @{};
$suggested = @{}
# $outdated is a list of ($app, $global) tuples
$outdated | ForEach-Object { update @_ $quiet $independent $suggested $use_cache $check_hash }
}

View File

@@ -29,15 +29,17 @@
# -p, --passthru Return reports as objects
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\json.ps1" # 'json_path'
. "$PSScriptRoot\..\lib\install.ps1" # 'hash_for_url'
. "$PSScriptRoot\..\lib\download.ps1" # 'hash_for_url'
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
if ($err) { "scoop virustotal: $err"; exit 1 }
if (!$apps -and -$all) { my_usage; exit 1 }
$architecture = Format-ArchitectureString
$all = $apps -eq '*' -or $opt.a -or $opt.all
if (!$apps -and !$all) { my_usage; exit 1 }
$architecture = Get-DefaultArchitecture
if (is_scoop_outdated) {
if ($opt.u -or $opt.'no-update-scoop') {
@@ -47,11 +49,8 @@ if (is_scoop_outdated) {
}
}
$apps_param = $apps
if ($apps_param -eq '*' -or $opt.a -or $opt.all) {
$apps = installed_apps $false
$apps += installed_apps $true
if ($all) {
$apps = (installed_apps $false) + (installed_apps $true)
}
if (!$opt.n -and !$opt.'no-depends') {
@@ -88,11 +87,6 @@ Function ConvertTo-VirusTotalUrlId ($url) {
$url_id
}
Function Get-RemoteFileSize ($url) {
$response = Invoke-WebRequest -Uri $url -Method HEAD -UseBasicParsing
$response.Headers.'Content-Length' | ForEach-Object { [System.Convert]::ToInt32($_) }
}
Function Get-VirusTotalResultByHash ($hash, $url, $app) {
$hash = $hash.ToLower()
$api_url = "https://www.virustotal.com/api/v3/files/$hash"

View File

@@ -1 +1 @@
410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe
140e3801d8adeda639a21b14e62b93a4c7d26b7a758421f43c82be59753be49b *shim.exe

View File

@@ -1 +1 @@
9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe
59d9da9f9714003b915bcafbe1b41f53b121dde206ecc23984f62273e957766eece8d64ffc53011c328d3a2ad627aa0f4f7c39bbec8e7b64d0d2ee7b7e771423 *shim.exe

Binary file not shown.

View File

@@ -1 +1 @@
v3.1.1
v3.1.2

View File

@@ -1,4 +1,4 @@
e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d *Newtonsoft.Json.dll
9f1a8f06c284a4ee01f704d89003ddc7061846f2008094071e9adf08267849f9 *Newtonsoft.Json.Schema.dll
d11b660612ce821ec03772b73aa3b8884a0479275c70085c7e143913a41a2d28 *Scoop.Validator.dll
7496d5349a123a6e3696085662b2ff17b156ccdb0e30e0c396ac72d2da36ce1c *Newtonsoft.Json.Schema.dll
83b1006443e8c340ca4c631614fc2ce0d5cb9a28c851e3b59724299f58b1397f *Scoop.Validator.dll
87f8f8db2202a3fbef6f431d0b7e20cec9d32095c441927402041f3c4076c1b6 *validator.exe

View File

@@ -1,4 +1,4 @@
56eb7f070929b239642dab729537dde2c2287bdb852ad9e80b5358c74b14bc2b2dded910d0e3b6304ea27eb587e5f19db0a92e1cbae6a70fb20b4ef05057e4ac *Newtonsoft.Json.dll
551e772fe2ee72b349d5c4ed5d5f8d8957d50cfcbbde7af5d5740d9652bcad626a2c00bc0d9223db7c874962187a90f9160397f243eadee1c594585ba2b155e0 *Newtonsoft.Json.Schema.dll
0a31d192c82bbd8ce50fb75dd5fe813c98bb870d54c112c600ae2e2436063cb2bd94bb206675dfe31ce89922e9a04a3d520ed579ab7198835190b67a6321a74e *Scoop.Validator.dll
78b12beb1e67ac4f6efa0fcba57b4b34ea6a31d8b369934d6b6a6617386ef9939ea453ac262916e5857ce0359eb809424ea33c676a87a8fdfd77a59b2ce96db0 *Newtonsoft.Json.Schema.dll
e9da4370aee4df47eedcf15d9749712eee513e5a9115b808617ddfcfde5bc47a0410edfb57508fcf51033c0be967611b2fd2c2ba944de7290c020cc67f77ac57 *Scoop.Validator.dll
58a0c37e98cac17822c7756bf6686a5fb74e711b8d986d13bd2f689f6b3b1f485fcd908d92cbc6a162a0e5974c2c5a43de57d15f1996be0aa405e41ec2ec8393 *validator.exe

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net45" />
<package id="Newtonsoft.Json.Schema" version="3.0.15" targetFramework="net45" />
<package id="Microsoft.Net.Compilers.Toolset" version="4.9.2" targetFramework="net45" developmentDependency="true" />
<package id="Newtonsoft.Json.Schema" version="4.0.1" targetFramework="net45" />
<package id="Microsoft.Net.Compilers.Toolset" version="4.10.0" targetFramework="net45"
developmentDependency="true" />
</packages>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import
Project="packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props"
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')" />
Project="packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props"
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -23,9 +23,8 @@
<Private>True</Private>
</Reference>
<Reference
Include="Newtonsoft.Json.Schema, Version=3.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>
packages\Newtonsoft.Json.Schema.3.0.15\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
Include="Newtonsoft.Json.Schema, Version=4.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>packages\Newtonsoft.Json.Schema.4.0.1\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@@ -52,7 +51,7 @@
http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')"
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props'))" />
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')"
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props'))" />
</Target>
</Project>

View File

@@ -1,8 +1,7 @@
BeforeAll {
. "$PSScriptRoot\Scoop-TestLib.ps1"
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\help.ps1"
. "$PSScriptRoot\..\libexec\scoop-alias.ps1" | Out-Null
. "$PSScriptRoot\..\lib\commands.ps1"
}
Describe 'Manipulate Alias' -Tag 'Scoop' {
@@ -15,32 +14,32 @@ Describe 'Manipulate Alias' -Tag 'Scoop' {
ensure $shimdir
}
It 'Creates a new alias if alias doesn''t exist' {
$alias_file = "$shimdir\scoop-rm.ps1"
$alias_file | Should -Not -Exist
It 'Creates a new alias if it does not exist' {
$alias_script = "$shimdir\scoop-rm.ps1"
$alias_script | Should -Not -Exist
add_alias 'rm' '"hello, world!"'
& $alias_file | Should -Be 'hello, world!'
& $alias_script | Should -Be 'hello, world!'
}
It 'Does not change existing file if its filename same as alias name' {
$alias_file = "$shimdir\scoop-rm.ps1"
It 'Skips an existing alias' {
$alias_script = "$shimdir\scoop-rm.ps1"
Mock abort {}
New-Item $alias_file -Type File -Force
$alias_file | Should -Exist
New-Item $alias_script -Type File -Force
$alias_script | Should -Exist
add_alias 'rm' '"test"'
Should -Invoke -CommandName abort -Times 1 -ParameterFilter { $msg -eq "File 'scoop-rm.ps1' already exists in shims directory." }
}
It 'Removes an existing alias' {
$alias_file = "$shimdir\scoop-rm.ps1"
$alias_file | Should -Exist
$alias_script = "$shimdir\scoop-rm.ps1"
$alias_script | Should -Exist
Mock get_config { @(@{'rm' = 'scoop-rm' }) }
Mock info {}
rm_alias 'rm'
$alias_file | Should -Not -Exist
$alias_script | Should -Not -Exist
Should -Invoke -CommandName info -Times 1 -ParameterFilter { $msg -eq "Removing alias 'rm'..." }
}
}

View File

@@ -73,28 +73,6 @@ Describe 'Test-HelperInstalled' -Tag 'Scoop' {
}
}
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
It 'should return true if aria2 is installed' {
Mock Test-HelperInstalled { $true }
Mock get_config { $true }
Test-Aria2Enabled | Should -BeTrue
}
It 'should return false if aria2 is not installed' {
Mock Test-HelperInstalled { $false }
Mock get_config { $false }
Test-Aria2Enabled | Should -BeFalse
Mock Test-HelperInstalled { $false }
Mock get_config { $true }
Test-Aria2Enabled | Should -BeFalse
Mock Test-HelperInstalled { $true }
Mock get_config { $false }
Test-Aria2Enabled | Should -BeFalse
}
}
Describe 'Test-CommandAvailable' -Tag 'Scoop' {
It 'should return true if command exists' {
Test-CommandAvailable 'Write-Host' | Should -BeTrue
@@ -259,6 +237,23 @@ Describe 'get_app_name_from_shim' -Tag 'Scoop', 'Windows' {
}
}
Describe 'cache_path' -Tag 'Scoop' {
It 'returns the correct cache path for a given input' {
$url = 'https://example.com/git.zip'
$ret = cache_path 'git' '2.44.0' $url
$inputStream = [System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($url))
$sha = (Get-FileHash -Algorithm SHA256 -InputStream $inputStream).Hash.ToLower().Substring(0, 7)
$ret | Should -Be "$cachedir\git#2.44.0#$sha.zip"
}
# # NOTE: Remove this 6 months after the feature ships.
It 'returns the old format cache path for a given input' {
Mock Test-Path { $true }
$ret = cache_path 'git' '2.44.0' 'https://example.com/git.zip'
$ret | Should -Be "$cachedir\git#2.44.0#https_example.com_git.zip"
}
}
Describe 'sanitary_path' -Tag 'Scoop' {
It 'removes invalid path characters from a string' {
$path = 'test?.json'

View File

@@ -14,7 +14,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
function test_extract($extract_fn, $from, $removal) {
$to = (strip_ext $from) -replace '\.tar$', ''
& $extract_fn ($from -replace '/', '\') ($to -replace '/', '\') -Removal:$removal
& $extract_fn ($from -replace '/', '\') ($to -replace '/', '\') -Removal:$removal -ExtractDir $args[0]
return $to
}
@@ -25,7 +25,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
}
It 'Test cases should exist and hash should match' {
$testcases | Should -Exist
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be '791bfce192917a2ff225dcdd87d23ae5f720b20178d85e68e4b1b56139cf8e6a'
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be '591072faabd419b77932b7023e5899b4e05c0bf8e6859ad367398e6bfe1eb203'
}
It 'Test cases should be extracted correctly' {
{ Microsoft.PowerShell.Archive\Expand-Archive -Path $testcases -DestinationPath $working_dir } | Should -Not -Throw
@@ -50,15 +50,42 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
$test6_1 = "$working_dir\7ZipTest6.part01.rar"
$test6_2 = "$working_dir\7ZipTest6.part02.rar"
$test6_3 = "$working_dir\7ZipTest6.part03.rar"
$test7 = "$working_dir\NSISTest.exe"
}
AfterEach {
Remove-Item -Path $to -Recurse -Force
}
It 'extract normal compressed file' {
$to = test_extract 'Expand-7zipArchive' $test1
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 4
}
It 'extract "extract_dir" correctly' {
$to = test_extract 'Expand-7zipArchive' $test1 $false 'tmp'
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'extract "extract_dir" with spaces correctly' {
$to = test_extract 'Expand-7zipArchive' $test1 $false 'tmp 2'
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'extract "extract_dir" with nested folder with same name' {
$to = test_extract 'Expand-7zipArchive' $test1 $false 'keep\sub'
$to | Should -Exist
"$to\keep\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
(Get-ChildItem "$to\keep").Count | Should -Be 1
}
It 'extract nested compressed file' {
# file ext: tgz
$to = test_extract 'Expand-7zipArchive' $test2
@@ -94,62 +121,45 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
(Get-ChildItem $to).Count | Should -Be 1
}
It 'extract NSIS installer' {
$to = test_extract 'Expand-7zipArchive' $test7
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'self-extract NSIS installer' {
$to = "$working_dir\NSIS Test"
$null = Invoke-ExternalCommand -FilePath $test7 -ArgumentList @('/S', '/NCRC', "/D=$to")
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'works with "-Removal" switch ($removal param)' {
$test1 | Should -Exist
test_extract 'Expand-7zipArchive' $test1 $true
$to = test_extract 'Expand-7zipArchive' $test1 $true
$to | Should -Exist
$test1 | Should -Not -Exist
$test5_1 | Should -Exist
$test5_2 | Should -Exist
$test5_3 | Should -Exist
test_extract 'Expand-7zipArchive' $test5_1 $true
$to = test_extract 'Expand-7zipArchive' $test5_1 $true
$to | Should -Exist
$test5_1 | Should -Not -Exist
$test5_2 | Should -Not -Exist
$test5_3 | Should -Not -Exist
$test6_1 | Should -Exist
$test6_2 | Should -Exist
$test6_3 | Should -Exist
test_extract 'Expand-7zipArchive' $test6_1 $true
$to = test_extract 'Expand-7zipArchive' $test6_1 $true
$to | Should -Exist
$test6_1 | Should -Not -Exist
$test6_2 | Should -Not -Exist
$test6_3 | Should -Not -Exist
}
}
Context 'zstd extraction' {
BeforeAll {
if ($env:CI) {
Mock Get-AppFilePath { $env:SCOOP_ZSTD_PATH } -ParameterFilter { $Helper -eq 'zstd' }
Mock Get-AppFilePath { '7z.exe' } -ParameterFilter { $Helper -eq '7zip' }
} elseif (!(installed zstd)) {
scoop install zstd
}
$test1 = "$working_dir\ZstdTest.zst"
$test2 = "$working_dir\ZstdTest.tar.zst"
}
It 'extract normal compressed file' {
$to = test_extract 'Expand-ZstdArchive' $test1
$to | Should -Exist
"$to\ZstdTest" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'extract nested compressed file' {
$to = test_extract 'Expand-ZstdArchive' $test2
$to | Should -Exist
"$to\ZstdTest" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
}
It 'works with "-Removal" switch ($removal param)' {
$test1 | Should -Exist
test_extract 'Expand-ZstdArchive' $test1 $true
$test1 | Should -Not -Exist
}
}
Context 'msi extraction' {
BeforeAll {
@@ -158,11 +168,13 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
} elseif (!(installed lessmsi)) {
scoop install lessmsi
}
Copy-Item "$working_dir\MSITest.msi" "$working_dir\MSI Test.msi"
$test1 = "$working_dir\MSITest.msi"
$test2 = "$working_dir\MSITestNull.msi"
$test2 = "$working_dir\MSI Test.msi"
$test3 = "$working_dir\MSITestNull.msi"
}
It 'extract normal MSI file' {
It 'extract normal MSI file using msiexec' {
Mock get_config { $false }
$to = test_extract 'Expand-MsiArchive' $test1
$to | Should -Exist
@@ -170,12 +182,32 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
(Get-ChildItem "$to\MSITest").Count | Should -Be 1
}
It 'extract empty MSI file using lessmsi' {
It 'extract normal MSI file with whitespace in path using msiexec' {
Mock get_config { $false }
$to = test_extract 'Expand-MsiArchive' $test2
$to | Should -Exist
"$to\MSITest\empty" | Should -Exist
(Get-ChildItem "$to\MSITest").Count | Should -Be 1
}
It 'extract normal MSI file using lessmsi' {
Mock get_config { $true }
$to = test_extract 'Expand-MsiArchive' $test1
$to | Should -Exist
}
It 'extract normal MSI file with whitespace in path using lessmsi' {
Mock get_config { $true }
$to = test_extract 'Expand-MsiArchive' $test2
$to | Should -Exist
}
It 'extract empty MSI file using lessmsi' {
Mock get_config { $true }
$to = test_extract 'Expand-MsiArchive' $test3
$to | Should -Exist
}
It 'works with "-Removal" switch ($removal param)' {
Mock get_config { $false }
$test1 | Should -Exist

View File

@@ -14,11 +14,6 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
Test-7zipRequirement -Uri 'test.bin' | Should -BeFalse
Test-7zipRequirement -Uri @('test.xz', 'test.bin') | Should -BeTrue
}
It 'Test Zstd requirement' {
Test-ZstdRequirement -Uri 'test.zst' | Should -BeTrue
Test-ZstdRequirement -Uri 'test.bin' | Should -BeFalse
Test-ZstdRequirement -Uri @('test.zst', 'test.bin') | Should -BeTrue
}
It 'Test lessmsi requirement' {
Mock get_config { $true }
Test-LessmsiRequirement -Uri 'test.msi' | Should -BeTrue
@@ -27,7 +22,6 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
}
It 'Allow $Uri be $null' {
Test-7zipRequirement -Uri $null | Should -BeFalse
Test-ZstdRequirement -Uri $null | Should -BeFalse
Test-LessmsiRequirement -Uri $null | Should -BeFalse
}
}
@@ -71,6 +65,7 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
BeforeAll {
Mock Test-HelperInstalled { $false }
Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' }
Mock get_config { $false } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' }
Mock Get-Manifest { 'lessmsi', @{}, $null, $null } -ParameterFilter { $app -eq 'lessmsi' }
Mock Get-Manifest { '7zip', @{ url = 'test.msi' }, $null, $null } -ParameterFilter { $app -eq '7zip' }
Mock Get-Manifest { 'innounp', @{}, $null, $null } -ParameterFilter { $app -eq 'innounp' }

View File

@@ -0,0 +1,49 @@
BeforeAll {
. "$PSScriptRoot\Scoop-TestLib.ps1"
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
}
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
It 'should return true if aria2 is installed' {
Mock Test-HelperInstalled { $true }
Mock get_config { $true }
Test-Aria2Enabled | Should -BeTrue
}
It 'should return false if aria2 is not installed' {
Mock Test-HelperInstalled { $false }
Mock get_config { $false }
Test-Aria2Enabled | Should -BeFalse
Mock Test-HelperInstalled { $false }
Mock get_config { $true }
Test-Aria2Enabled | Should -BeFalse
Mock Test-HelperInstalled { $true }
Mock get_config { $false }
Test-Aria2Enabled | Should -BeFalse
}
}
Describe 'url_filename' -Tag 'Scoop' {
It 'should extract the real filename from an url' {
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
}
It 'can be tricked with a hash to override the real filename' {
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
}
}
Describe 'url_remote_filename' -Tag 'Scoop' {
It 'should extract the real filename from an url' {
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
}
It 'can not be tricked with a hash to override the real filename' {
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
}
}

View File

@@ -12,28 +12,6 @@ Describe 'appname_from_url' -Tag 'Scoop' {
}
}
Describe 'url_filename' -Tag 'Scoop' {
It 'should extract the real filename from an url' {
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
}
It 'can be tricked with a hash to override the real filename' {
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
}
}
Describe 'url_remote_filename' -Tag 'Scoop' {
It 'should extract the real filename from an url' {
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
}
It 'can not be tricked with a hash to override the real filename' {
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
}
}
Describe 'is_in_dir' -Tag 'Scoop', 'Windows' {
It 'should work correctly' {
is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse

View File

@@ -1,15 +1,25 @@
BeforeAll {
. "$PSScriptRoot\..\lib\json.ps1"
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\buckets.ps1"
. "$PSScriptRoot\..\lib\database.ps1"
. "$PSScriptRoot\..\lib\autoupdate.ps1"
}
Describe 'JSON parse and beautify' -Tag 'Scoop' {
Context 'Parse JSON' {
It 'success with valid json' {
{ parse_json "$PSScriptRoot\fixtures\manifest\wget.json" } | Should -Not -Throw
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\wget.json"
$parsed | Should -Not -Be $null
}
It 'fails with invalid json' {
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Throw
It 'returns null and warns with invalid json' {
Mock warn {}
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Not -Throw
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json"
$parsed | Should -Be $null
Should -Invoke -CommandName warn -Times 1
}
}
Context 'Beautify JSON' {
@@ -84,3 +94,144 @@ Describe 'Manifest Validator' -Tag 'Validator' {
$validator.Errors | Select-Object -Last 1 | Should -Match 'Required properties are missing from object: version\.'
}
}
Describe 'Get-RelativePathCompat' -Tag 'Scoop' {
It 'returns relative path for child path' {
$from = 'C:\root\bucket'
$to = 'C:\root\bucket\foo\bar.json'
Get-RelativePathCompat $from $to | Should -Be 'foo\bar.json'
}
It 'returns original when different drive/scheme' {
$from = 'C:\root\bucket'
$to = 'D:\other\file.json'
Get-RelativePathCompat $from $to | Should -Be $to
}
}
Describe 'Find-HistoricalManifestInCache' -Tag 'Scoop' {
It 'returns $null when sqlite cache disabled' {
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.0.0'
$result | Should -Be $null
}
It 'returns manifest text and version when cache has exact match' {
$tempUM = Join-Path $env:TEMP 'ScoopTestsUM'
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Get-ScoopDBItem {
$dt = New-Object System.Data.DataTable
[void]$dt.Columns.Add('manifest')
$row = $dt.NewRow()
$row['manifest'] = '{"version":"1.2.3"}'
[void]$dt.Rows.Add($row)
Write-Output $dt -NoEnumerate
}
Mock ensure {}
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.2.3'
$result | Should -Not -BeNullOrEmpty
$result.version | Should -Be '1.2.3'
$result.ManifestText | Should -Match '"version":"1.2.3"'
}
}
Describe 'Find-HistoricalManifestInGit' -Tag 'Scoop' {
It 'returns $null when git history search disabled' {
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $false }
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
$result | Should -Be $null
}
It 'returns manifest text on version match' {
$bucketRoot = 'C:\b\root'
$innerBucket = 'C:\b\root\bucket'
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $true }
Mock Find-BucketDirectory -ParameterFilter { $Root } { $bucketRoot }
Mock Find-BucketDirectory -ParameterFilter { -not $Root } { $innerBucket }
Mock Test-Path -ParameterFilter { $Path -eq (Join-Path $bucketRoot '.git') } { $true }
Mock Test-Path -ParameterFilter { $Path -eq $innerBucket -and $PathType -eq 'Container' } { $true }
# Behavior-oriented mocks: using HEAD should yield a wrong version
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' -and $ArgumentList[1] -like 'HEAD*' } { $global:LASTEXITCODE = 0; return '{"version":"2.0.0"}' }
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' } { $global:LASTEXITCODE = 0; return '{"version":"1.0.0"}' }
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'log' } { @('abcdef0123456789') }
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
$result | Should -Not -BeNullOrEmpty
$result.version | Should -Be '1.0.0'
$result.ManifestText | Should -Match '"version":"1.0.0"'
}
}
Describe 'generate_user_manifest (history-aware)' -Tag 'Scoop' {
It 'returns manifest_path when versions match' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='1.0.0' }, 'main', $null }
Mock manifest_path { 'C:\path\foo.json' }
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\path\foo.json'
}
It 'prefers history orchestrator hit (cache) when enabled' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Find-HistoricalManifest { @{ path = 'C:\cache\foo.json'; version = '1.0.0'; source='sqlite_exact_match' } }
Mock info {}
Mock warn {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\cache\foo.json'
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
}
It 'falls back to git history when cache misses' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Find-HistoricalManifest { @{ path = 'C:\git\foo.json'; version = '1.0.0'; source='git_manifest:hash' } }
Mock info {}
Mock warn {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\git\foo.json'
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
}
It 'uses autoupdate when no history found and autoupdate exists' {
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock ensure {}
Mock usermanifestsdir { $umdir }
Mock Invoke-AutoUpdate {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be (Join-Path $umdir 'foo.json')
}
It 'on autoupdate failure aborts with concise message' {
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock ensure {}
Mock usermanifestsdir { $umdir }
Mock Invoke-AutoUpdate { throw 'fail' }
Mock warn {}
Mock info {}
Mock Write-Host {}
Mock abort { throw 'aborted' }
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
}
It 'aborts when no history and no autoupdate' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock warn {}
Mock info {}
Mock abort { throw 'aborted' }
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
}
}

View File

@@ -1,13 +1,13 @@
# copies fixtures to a working directory
function setup_working($name) {
$fixtures = "$PSScriptRoot/fixtures/$name"
$fixtures = "$PSScriptRoot\fixtures\$name"
if (!(Test-Path $fixtures)) {
Write-Host "couldn't find fixtures for $name at $fixtures" -f red
exit 1
}
# reset working dir
$working_dir = "$([IO.Path]::GetTempPath())ScoopTestFixtures/$name"
$working_dir = "$([IO.Path]::GetTempPath())ScoopTestFixtures\$name"
if (Test-Path $working_dir) {
Remove-Item -Recurse -Force $working_dir

View File

@@ -70,19 +70,6 @@ if ($env:CI -eq $true) {
Invoke-WebRequest -Uri $source -OutFile $destination
& 7z.exe x "$env:SCOOP_HELPERS_PATH\innounp.rar" -o"$env:SCOOP_HELPERS_PATH\innounp" -y | Out-Null
}
# Only download zstd for AppVeyor, GitHub Actions has zstd installed by default
if ($env:BHBuildSystem -eq 'AppVeyor') {
$env:SCOOP_ZSTD_PATH = "$env:SCOOP_HELPERS_PATH\zstd\zstd.exe"
if (!(Test-Path $env:SCOOP_ZSTD_PATH)) {
$source = 'https://github.com/facebook/zstd/releases/download/v1.5.1/zstd-v1.5.1-win32.zip'
$destination = "$env:SCOOP_HELPERS_PATH\zstd.zip"
Invoke-WebRequest -Uri $source -OutFile $destination
& 7z.exe x "$env:SCOOP_HELPERS_PATH\zstd.zip" -o"$env:SCOOP_HELPERS_PATH\zstd" -y | Out-Null
}
} else {
$env:SCOOP_ZSTD_PATH = (Get-Command zstd.exe).Path
}
}
}

Binary file not shown.