# Copyright Kevin Deldycke <kevin@deldycke.com> 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 extra_platforms import UNIX_WITHOUT_MACOS
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 Snap(PackageManager):
"""Canonical's snap installs sandboxed, self-updating packages.
snaps refresh themselves on a schedule, so `upgrade` merely forces a
`snap refresh` sooner. Mutating operations escalate through `sudo` by
default: the privileged work happens in the `snapd` daemon, but the daemon
refuses state changes from an unprivileged client (`snap install` as a
plain user is denied with a hint to retry with `sudo`), which is why the
Snap store documents `sudo snap install` as the canonical invocation. A
host authenticated against the store with `snap login` (or granted a
polkit rule) can drop the wrap with `--no-sudo` or a
`[mpm.managers.snap] sudo = false` override.
```{note}
snap localizes and colorizes its table headers with no terminal
detection. mpm pins nothing to English: `--color=never` strips the
ANSI, the header row is dropped, and every row is split on whitespace
and read by column position, so the translated headers never reach the
parser.
```
```{note}
`snap refresh --list` reports only the available version, so
`outdated` looks each installed version up by ID from the cached
installed set. `search` runs `snap find`, which matches summaries as
well as names, so mpm refilters the results.
```
"""
homepage_url = "https://snapcraft.io"
logo = "snapcraft"
platforms = UNIX_WITHOUT_MACOS
default_sudo = True
requirement = ">=2.0.0"
post_args = ("--color=never",)
_SEARCH_REGEXP = re.compile(
r"^(?P<package_id>\S+)\s+(?P<version>\S+)\s+\S+\s+\S+\s+(?P<description>.+)$",
re.MULTILINE,
)
version_regexes = (r"snap\s+(?P<version>\S+)",)
"""
```{code-block} shell-session
$ snap --version
snap 2.44.1
snapd 2.44.1
series 16
linuxmint 19.3
kernel 4.15.0-91-generic
```
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} shell-session
$ snap list --color=never
Name Version Rev Aufzeichnung Herausgeber Hinweise
core 16-2.44.1 8935 latest/stable canonical✓ core
wechat 2.0 7 latest/stable ubuntu-dawndiy -
pdftk 2.02-4 9 latest/stable smoser -
```
"""
output = self.run_cli("list")
for package in output.splitlines()[1:]:
parts = package.split()
yield self.package(id=parts[0], installed_version=parts[1])
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{code-block} shell-session
$ snap refresh --list --color=never
Name Version Rev Herausgeber Hinweise
standard-notes 3.3.5 8 standardnotes✓ -
```
"""
output = self.run_cli("refresh", "--list")
installed_versions = self.installed_version_map
for package in output.splitlines()[1:]:
parts = package.split()
package_id = parts[0]
yield self.package(
id=package_id,
latest_version=parts[1],
installed_version=installed_versions.get(package_id),
)
[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 is extended by default. So we return the best subset of results and
let {meth}`meta_package_manager.manager.PackageManager.refiltered_search`
refine them.
```
```{code-block} shell-session
$ snap find doc --color=never
Name Version Herausgeber Hinweise Zusammenfassung
journey 2.14.3 2appstudio - Your private diary.
nextcloud 17.0.5snap1 nextcloud✓ - Nextcloud Server
skype 8.58.0.93 skype✓ classic One Skype for all.
```
"""
output = self.run_cli("find", query)
headerless_table = None
if output:
table = output.split("\n", 1)
if len(table) > 1:
headerless_table = table[1]
if headerless_table:
for package_id, version, description in self._SEARCH_REGEXP.findall(
headerless_table,
):
yield self.package(
id=package_id,
description=description,
latest_version=version,
)
[docs]
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
```{code-block} shell-session
$ sudo snap install standard-notes --color=never
```
"""
return self.run_cli("install", package_id, sudo=True)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all outdated packages.
```{code-block} shell-session
$ sudo snap refresh --color=never
```
"""
return self.build_cli("refresh", sudo=True)
[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} shell-session
$ sudo snap refresh standard-notes --color=never
```
"""
return self.build_cli("refresh", package_id, sudo=True)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
Unlike `list`, `find`, `refresh` and `install`, snap's `remove`
subcommand rejects the global `--color` flag (`error: unknown flag
'color'`). Its output is not parsed, so the `--color=never` post-arg
is dropped here with `auto_post_args=False`.
```{code-block} shell-session
$ sudo snap remove standard-notes
```
"""
return self.run_cli("remove", package_id, sudo=True, auto_post_args=False)