# 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 datetime import datetime
from extra_platforms import UNIX_WITHOUT_MACOS
from ..capabilities import search_capabilities, version_not_implemented
from ..execution import CLIError
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}
A `--brewfile` dump emits the bare `flatpak "id"` form, mpm capturing no
origin remote per app. See {doc}`/dump`, section "Flatpak remote".
```
"""
homepage_url = "https://flatpak.org"
logo = "flatpak"
keywords = ("flathub",)
brewfile_entry_type = "flatpak"
"""Mapped to Homebrew Bundle's `flatpak` extension.
Its `with: ["remote"]` keyword goes unused, {meth}`~meta_package_manager.manager.PackageManager.installed` capturing no
origin remote to fill it with. See {doc}`/dump`, section "Flatpak remote".
"""
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>.+)
""",
# Walked over the whole output by `findall`, so `^` has to bind to each
# line: without this only the first result is ever matched.
re.VERBOSE | re.MULTILINE,
)
_ORIGIN_REGEXP = re.compile(r"^\s*Origin:\s*(?P<remote>\S+)", re.MULTILINE)
_REMOTE_DATE_REGEXP = re.compile(
r"^\s*Date:\s*(?P<date>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4})",
re.MULTILINE,
)
_C_LOCALE_ENV = {"LC_ALL": "C.UTF-8"}
"""Locale forced on the release-date probes below.
The `Origin:` and `Date:` field labels parsed by {meth}`release_date` are
localized by flatpak, and the probes key on their English spelling.
"""
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]
def release_date(self, package_id: str) -> datetime | None:
"""Publication timestamp of the latest build of an app, read off its remote.
The date is the ostree commit timestamp of the newest commit on the
app's branch, reported by `flatpak remote-info` and rendered in UTC
with a literal `+0000` offset whatever the host locale. The remote's
build service stamps it at publication (Flathub's buildbot for the
`flathub` remote), so the app's author cannot backdate it, which is
what makes it a sound cooldown clock. On a third-party remote the
publisher stamps it instead: the probe is then only as trustworthy as
the remote itself, exactly like the packages it serves.
The probe resolves the remote to interrogate from the installed app's
origin, and falls back to trying every configured remote for an app
not installed yet (an `install` gated before its first installation).
On a host with several remotes, that fallback logs one error per
remote not serving the app.
```{code-block} console
$ flatpak remote-info --ostree-verbose flathub org.gnome.Calculator
Calculator - Perform arithmetic, scientific or financial calculations
ID: org.gnome.Calculator
Ref: app/org.gnome.Calculator/aarch64/stable
Arch: aarch64
Branch: stable
Version: 50.0
License: GPL-3.0-or-later
Collection: org.flathub.Stable
Download Size: 1.8 MB
Installed Size: 4.9 MB
Runtime: org.gnome.Platform/aarch64/50
Sdk: org.gnome.Sdk/aarch64/50
Commit: 473c4d6d2d553eaeba13c0f1fd27cc9af0c4362cb4ad2a7050149930e89f3eb1
Parent: 6f12ad4cd67efce88bf63ec51c13a9b6a1891c61d8ab640b85ce25d0f37f089c
Subject: Merge pull request #52 from flathub/update-master-19cd934 (ee57e22e7a51)
Date: 2026-03-18 19:40:01 +0000
```
"""
remotes = []
if package_id in self.installed_ids:
origin_output = self.run_cli(
"info",
"--ostree-verbose",
package_id,
override_extra_env=self._C_LOCALE_ENV,
)
origin = self._ORIGIN_REGEXP.search(origin_output)
if origin:
remotes.append(origin.group("remote"))
if not remotes:
remotes = self.run_cli(
"remotes",
"--columns=name",
"--ostree-verbose",
override_extra_env=self._C_LOCALE_ENV,
).split()
for remote in remotes:
try:
output = self.run_cli(
"remote-info",
"--ostree-verbose",
remote,
package_id,
override_extra_env=self._C_LOCALE_ENV,
)
except CLIError:
# The app is not served by this remote: try the next one.
continue
match = self._REMOTE_DATE_REGEXP.search(output)
if match:
return datetime.strptime(match.group("date"), "%Y-%m-%d %H:%M:%S %z")
return None
[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")