# 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 Flatpak(PackageManager):
"""Flatpak manages sandboxed desktop applications pulled from remotes like
Flathub.
mpm covers applications only: every listing passes `--app`, so runtimes
and SDKs stay out of scope. Listings are requested with
`--columns=name,application,version --ostree-verbose` and parsed as
tab-separated rows.
```{note}
All operations target the system-wide scope except `cleanup` which only
repairs the user installation. Per-scope targeting (system vs user) is
tracked in [#1725](https://github.com/kdeldycke/meta-package-manager/issues/1725).
```
```{note}
Escalation is polkit's job, so no operation is marked `sudo`: flatpak
hands system-scope mutations to its privileged system helper over D-Bus,
which authorizes them through polkit (Flathub documents plain
`flatpak install`). Under a strict polkit policy, unattended mutations
need a rule permitting them without interactive authentication.
```
```{caution}
`outdated` reads each pending update's latest version from
`remote-ls --updates`, then runs one `flatpak info` per package to
recover its installed version: a follow-up CLI call for every outdated
app.
```
```{note}
Brewfile backups emit the bare `flatpak "id"` form: mpm does not
capture each app's origin remote, so `brew bundle install` restores
through Flathub and non-Flathub apps must be edited in by hand.
```
"""
homepage_url = "https://flatpak.org"
logo = "flatpak"
brewfile_entry_type = "flatpak"
"""Mapped to Homebrew Bundle's `flatpak` extension.
The Brewfile `flatpak` entry supports a `with: ["remote"]` keyword for
non-default remotes. mpm's {meth}`installed` does not currently capture the
origin remote per package, so the dump emits the bare `flatpak "id"` form;
`brew bundle install` then resolves through the default `flathub` remote.
Non-flathub flatpaks need to be edited in by hand after the dump.
"""
platforms = UNIX_WITHOUT_MACOS
requirement = ">=1.2.0"
_LIST_REGEXP = re.compile(
r"(?P<name>.+?)\t(?P<package_id>\S+)\t?(?P<latest_version>.*)",
)
_SEARCH_REGEXP = re.compile(
r"""
^(?P<package_name>\S+)\t
(?P<description>.+)\t
(?P<package_id>\S+)\t
(?P<version>\S+)\t
(?P<branch>\S+)\t
(?P<remotes>.+)
""",
re.VERBOSE,
)
version_regexes = (r"Flatpak\s+(?P<version>\S+)",)
"""
```{code-block} shell-session
$ flatpak --version
Flatpak 1.4.2
```
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} shell-session
$ flatpak list --app --columns=name,application,version \
> --ostree-verbose
Peek com.uploadedlobster.peek 1.3.1
Fragments de.haeckerfelix.Fragments 1.4
GNOME MPV io.github.GnomeMpv 0.16
Syncthing GTK me.kozec.syncthingtk v0.9.4.3
Builder org.flatpak.Builder
```
"""
output = self.run_cli(
"list",
"--app",
"--columns=name,application,version",
"--ostree-verbose",
)
for package in output.splitlines():
match = self._LIST_REGEXP.match(package)
if match:
name, package_id, installed_version = match.groups()
yield self.package(
id=package_id,
name=name,
installed_version=installed_version,
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{code-block} shell-session
$ flatpak remote-ls --app --updates --columns=name,application,version \
--ostree-verbose
GNOME Dictionary org.gnome.Dictionary 3.26.0
Files org.gnome.Nautilus 42.2
```
"""
output = self.run_cli(
"remote-ls",
"--app",
"--updates",
"--columns=name,application,version",
"--ostree-verbose",
)
for package in output.splitlines():
match = self._LIST_REGEXP.match(package)
if match:
name, package_id, latest_version = match.groups()
info_installed_output = self.run_cli(
"info",
"--ostree-verbose",
package_id,
)
current_version = re.search(
r"version:\s(?P<version>\S.*?)\n",
info_installed_output,
re.IGNORECASE,
)
installed_version = (
current_version.group("version") if current_version else None
)
yield self.package(
id=package_id,
name=name,
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} shell-session
$ flatpak search gitg --ostree-verbose
gitg GUI for git org.gnome.gitg 3.32.1 stable flathub
```
"""
output = self.run_cli("search", query, "--ostree-verbose")
for (
package_name,
description,
package_id,
version,
_branch,
_remotes,
) in self._SEARCH_REGEXP.findall(output):
yield self.package(
id=package_id,
name=package_name,
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
$ flatpak install --noninteractive org.gnome.Dictionary
```
"""
return self.run_cli("install", "--noninteractive", package_id)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all outdated packages.
```{code-block} shell-session
$ flatpak update --noninteractive
```
"""
return self.build_cli("update", "--noninteractive")
[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
$ flatpak update --noninteractive org.gnome.Dictionary
```
"""
return self.build_cli("update", "--noninteractive", package_id)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
```{code-block} shell-session
$ flatpak uninstall --noninteractive org.gnome.Dictionary
```
"""
return self.run_cli("uninstall", "--noninteractive", package_id)
[docs]
def cleanup_orphan(self) -> None:
"""Uninstall runtimes and extensions no longer used by any installed app.
```{code-block} shell-session
$ flatpak uninstall --unused --noninteractive
```
"""
self.run_cli("uninstall", "--unused", "--noninteractive")
[docs]
def cleanup_repair(self) -> None:
"""Verify and repair the per-user installation.
See the [`flatpak repair` reference](https://docs.flatpak.org/en/latest/flatpak-command-reference.html#flatpak-repair).
```{code-block} shell-session
$ flatpak repair --user
```
"""
self.run_cli("repair", "--user")
[docs]
def doctor_cli(self) -> tuple[str, ...]:
"""Generates the CLI running the native self-diagnosis.
The read-only twin of {meth}`cleanup_repair`: `--dry-run` reports
what a repair of the per-user installation would fix without touching it.
```{code-block} shell-session
$ flatpak repair --user --dry-run
```
"""
return self.build_cli("repair", "--user", "--dry-run")