Source code for meta_package_manager.managers.scoop

# 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 ``` """
[docs] @staticmethod def remove_headers(text: str) -> str: results = text.split("---\n", 1) if len(results) != 2: return "" return results[1]
@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")