# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
import re
from typing import ClassVar
from extra_platforms import WINDOWS
from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Scoop(PackageManager):
"""Scoop is a user-level command-line installer for Windows.
Apps unpack under `~/scoop` without elevation, so no operation is marked
`sudo`. Escalating would be fatal: the installer refuses to run as an
administrator at all, answering `Running the installer as administrator is
disabled by default` and aborting.
"""
# The `installed`, `outdated` and `search` listings are column tables
# introduced by a `---` separator line: mpm drops everything up to that
# separator, then splits each row positionally on whitespace.
#
# `install` with a version passes `<package>@<version>`: Scoop installs it
# from a generated manifest, which `scoop status` and `scoop update` then
# compare the package against, so it never reads as outdated.
#
# `remove` uninstalls with `--purge`, so a package's persisted data
# directory is deleted with it rather than kept for a later reinstall.
operation_notes: ClassVar = {
"install": (
"A version installs the manifest that shipped it, which pins the "
"package: `outdated` and `upgrade` then skip it."
),
"remove": (
"The `remove` command uninstalls with `--purge`, so a package's "
"persisted data directory is deleted rather than kept for a "
"later reinstall."
),
}
name = "Scoop"
homepage_url = "https://scoop.sh"
documentation_url = "https://github.com/ScoopInstaller/Scoop/wiki"
repository_url = "https://github.com/ScoopInstaller/Scoop"
wikipedia_url = "https://en.wikipedia.org/wiki/Scoop_Package_Manager"
# No `logo`: Simple Icons carries no Scoop mark yet. Of the marks missing from
# the pool, this request is the likeliest to land, now that
# https://scoop.sh/logo/scoop.svg gives them an official vector to redraw from:
# https://github.com/simple-icons/simple-icons/issues/13589
platforms = WINDOWS
# `0.6.0` fixed `scoop --version`, which printed the raw `git log` line of
# the checkout instead of a clean version:
# https://github.com/ScoopInstaller/Scoop/issues/6457.
requirement = ">=0.6.0"
_LIST_REGEXP = re.compile(
r"""
(?P<package_id>\S+) # Any string.
\s+ # Any number of blank chars.
(?P<version>\S+) # Version string.
\s+ # Any number of blank chars.
.+ # Any string.
""",
re.VERBOSE,
)
_OUTDATED_REGEXP = re.compile(
r"""
(?P<package_id>\S+) # Any string.
\ + # One space or more.
(?P<installed_version>\S*) # Version string.
\ + # One space or more.
(?P<latest_version>\S*) # Version string.
""",
re.VERBOSE,
)
_SEARCH_REGEXP = re.compile(
r"""
(?P<package_id>\S+) # Any string.
\ + # One space or more.
(?P<version>\S+) # Version string.
\ + # One space or more.
\S+ # Any string.
""",
re.VERBOSE,
)
version_regexes = (r"^v(?P<version>\S+)\s.+",)
"""Search version at the start of a line.
```{code-block} pwsh-session
> scoop --version
Current Scoop version:
v0.6.0 - Released at 2026-09-30
'main' bucket:
22fbffff92 tombi: Update to version 1.6.1
'extras' bucket:
a149ee2c79 suwayomi-server: Update to version 2.4.2366
```
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} pwsh-session
> scoop list
Installed apps:
Name Version Source Updated Info
---- ------- ------ ------- ----
7zip 22.01 main 2022-09-27 08:03:30
dark 3.11.2 main 2022-09-27 08:04:26
git 2.37.3.windows.1 main 2022-09-27 08:03:58
python 3.10.7 main 2022-09-27 08:04:53
```
"""
output = self.run_cli("list")
for package_id, version in self._LIST_REGEXP.findall(
self.remove_headers(output)
):
yield self.package(id=package_id, installed_version=version)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{code-block} pwsh-session
> scoop status
Name Installed Version Latest Version Missing Dependencies Info
---- ----------------- -------------- -------------------- ----
demulshooter 16.7.2 18.7.3
eduke32 20220611-10112 20220709-10115
Teracopy-np
yuzu-pineapple EA-2804 EA-2830
```
"""
output = self.run_cli("status")
for (
package_id,
installed_version,
latest_version,
) in self._OUTDATED_REGEXP.findall(
self.remove_headers(output),
):
yield self.package(
id=package_id,
latest_version=latest_version,
installed_version=installed_version,
)
[docs]
@search_capabilities(extended_support=False, exact_support=False)
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
```{caution}
Search does not support extended or exact matching. So we return the best
subset of results and let
{meth}`meta_package_manager.manager.PackageManager.refiltered_search` refine
them.
```
```{code-block} pwsh-session
> scoop search zip
Results from local buckets...
Name Version Source Binaries
---- ------- ------ --------
7zip 22.01 main
7zip19.00-helper 19.00 main
busybox 4716-g31467ddfc main bunzip2 | gunzip | gzip | unzip
bzip2 1.0.8.0 main
gow 0.8.0 main bunzip2.exe | bzip2.exe | zip.exe
gzip 1.3.12 main
lzip 1.20 main
unzip 6.00 main
zip 3.0 main
```
"""
output = self.run_cli("search", query)
for package_id, version in self._SEARCH_REGEXP.findall(
self.remove_headers(output),
):
yield self.package(id=package_id, latest_version=version)
[docs]
def install(self, package_id: str, version: str | None = None) -> str:
r"""Install one package.
A `version` installs the manifest that shipped it, which Scoop looks up
in the bucket's Git history before it falls back to generating one from
the `autoupdate` rules of the current manifest.
```{code-block} pwsh-session
> scoop install 7zip
Installing '7zip' (22.01) [64bit] from main bucket
7z2201-x64.msi (1.8 MB) [====================] 100%
Checking hash of 7z2201-x64.msi ... ok.
Extracting 7z2201-x64.msi ... done.
Linking ~\scoop\apps\7zip\current => ~\scoop\apps\7zip\22.01
Creating shim for '7z'.
Creating shortcut for 7-Zip (7zFM.exe)
Persisting Codecs
Persisting Formats
Running post_install script...
'7zip' (22.01) was installed successfully!
Notes
-----
Add 7-Zip as a context menu by running: "C:\scoop\...install-context.reg"
```
```{code-block} pwsh-session
> scoop install [email protected]
INFO Resolving historical manifest for 'hyperfine' (1.18.0)
Installing 'hyperfine' (1.18.0) [64bit] from generated manifest
Downloading https://github.com/sharkdp/hyperfine/releases/download/v1.18.0/hyperfine-v1.18.0-x86_64-pc-windows-msvc.zip (569.1 KB)...
Checking hash of hyperfine-v1.18.0-x86_64-pc-windows-msvc.zip... OK.
Extracting hyperfine-v1.18.0-x86_64-pc-windows-msvc.zip... Done.
Linking ~\scoop\apps\hyperfine\current => ~\scoop\apps\hyperfine\1.18.0
Creating shim for 'hyperfine'.
'hyperfine' (1.18.0) was installed successfully!
```
"""
return self.run_cli(
"install", f"{package_id}@{version}" if version else package_id
)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all outdated packages.
```{code-block} pwsh-session
> scoop update --all
```
"""
return self.build_cli("update", "--all")
[docs]
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generates the CLI to upgrade the provided package.
```{code-block} pwsh-session
> scoop update 7zip
```
"""
return self.build_cli("update", package_id)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
```{code-block} pwsh-session
> scoop uninstall 7zip --purge
Uninstalling '7zip' (22.01).
Removing shim '7z.shim'.
Removing shim '7z.exe'.
Removing shortcut ~\\AppData\\Roaming\\Scoop Apps\7-Zip.lnk
Unlinking ~\\scoop\apps\7zip\\current
'7zip' was uninstalled.
```
"""
return self.run_cli("uninstall", package_id, "--purge")
[docs]
def sync(self) -> None:
"""Sync package metadata.
```{code-block} console
> scoop status
WARN Scoop out of date. Run 'scoop update' to get the latest changes.
```
```{code-block} pwsh-session
> scoop update
Updating Scoop...
Updating 'main' bucket...
Converting 'main' bucket to git repo...
Checking repo... OK
The main bucket was added successfully.
Scoop was updated successfully!
```
```{code-block} console
> scoop status
Scoop is up to date.
Everything is ok!
```
"""
self.run_cli("update")
[docs]
def cleanup_cache(self) -> None:
"""Removes things we don't need anymore.
```{code-block} pwsh-session
> scoop cleanup --all --cache
Everything is shiny now!
```
"""
self.run_cli("cleanup", "--all", "--cache")
[docs]
def doctor_cli(self) -> tuple[str, ...]:
"""Generates the CLI running the native self-diagnosis.
```{caution}
`checkup` prints its suggestions but is optimistic with its exit
code, so an unhealthy setup may still report as `✓`: the relayed
report is the signal to read.
```
```{code-block} pwsh-session
> scoop checkup
```
"""
return self.build_cli("checkup")