Fixed the management of flatpak packages installed to the user installation as requested in the PR

This commit is contained in:
David Groniewsky
2025-10-09 22:42:19 +02:00
parent d765eea3f4
commit 7ea1ef2d81
5 changed files with 99 additions and 51 deletions
+2 -2
View File
@@ -116,8 +116,8 @@ class MyModule(Module):
def flatpak_packages(self) -> list[str]: def flatpak_packages(self) -> list[str]:
return ["org.mozilla.firefox"] return ["org.mozilla.firefox"]
def flatpak_user_packages(self) -> list[str]: def flatpak_user_packages(self) -> list[tuple[str,str]]:
return ["io.github.kolunmi.Bazaar"] return [("username", "io.github.kolunmi.Bazaar")]
def systemd_units(self) -> list[str]: def systemd_units(self) -> list[str]:
return ["reflector.timer"] return ["reflector.timer"]
+3 -1
View File
@@ -38,7 +38,9 @@ decman.flatpak_packages += ["dev.qwery.AddWater"]
decman.ignored_flatpak_packages += ["org.signal.Signal"] decman.ignored_flatpak_packages += ["org.signal.Signal"]
# You can also install them to your user installation instead of the system installation # You can also install them to your user installation instead of the system installation
decman.flatpak_user_packages += ["dev.zed.Zed"] decman.flatpak_user_packages += [
("username", "dev.zed.Zed")
]
# To import GPG keys, set the GNUPGHOME environment variable. # To import GPG keys, set the GNUPGHOME environment variable.
# It can easily be done with python as well. # It can easily be done with python as well.
+2 -2
View File
@@ -387,7 +387,7 @@ class Module:
""" """
return [] return []
def flatpak_user_packages(self) -> list[str]: def flatpak_user_packages(self) -> list[tuple[str, str]]:
""" """
Override this method to return flatpak packages that should be installed to the user installation as a part of this Override this method to return flatpak packages that should be installed to the user installation as a part of this
Module. Module.
@@ -427,5 +427,5 @@ files: dict[str, File] = {}
directories: dict[str, Directory] = {} directories: dict[str, Directory] = {}
modules: list[Module] = [] modules: list[Module] = []
flatpak_packages: list[str] = [] flatpak_packages: list[str] = []
flatpak_user_packages: list[str] = [] flatpak_user_packages: list[tuple[str, str]] = []
ignored_flatpak_packages: list[str] = [] ignored_flatpak_packages: list[str] = []
+34 -12
View File
@@ -6,8 +6,10 @@ Module containing the CLI Application.
import argparse import argparse
import os import os
import shutil import shutil
import subprocess
import sys import sys
import traceback import traceback
import pwd
import decman import decman
import decman.config as conf import decman.config as conf
@@ -285,16 +287,12 @@ class Core:
to_remove_flatpak = self.source.flatpak_packages_to_remove( to_remove_flatpak = self.source.flatpak_packages_to_remove(
currently_installed_flatpak currently_installed_flatpak
) )
currently_installed_user_flatpak = self.flatpak.get_installed(True)
to_remove_user_flatpak = self.source.flatpak_packages_to_remove(
currently_installed_user_flatpak, as_user=True
)
l.print_list("Removing pacman packages:", to_remove) l.print_list("Removing pacman packages:", to_remove)
if conf.enable_flatpak: if conf.enable_flatpak:
l.print_list("Removing flatpak packages:", to_remove_flatpak) l.print_list("Removing flatpak packages:", to_remove_flatpak)
l.print_list("Removing user flatpak packages:", to_remove_user_flatpak) self._remove_user_flatpaks(only_print=True)
if self.only_print: if self.only_print:
return return
@@ -304,7 +302,21 @@ class Core:
# flatpak # flatpak
if conf.enable_flatpak and self.update_flatpaks: if conf.enable_flatpak and self.update_flatpaks:
self.flatpak.remove(to_remove_flatpak) self.flatpak.remove(to_remove_flatpak)
self.flatpak.remove(to_remove_user_flatpak, True) self._remove_user_flatpaks()
def _remove_user_flatpaks(self, only_print: bool = False):
# get all users through a command instead of pwd because pwd also lists all 'virtual' users. add root since they can also have user installed packages
users = subprocess.run(["users"], check=True, stdout=subprocess.PIPE).stdout.decode().strip().split('\n')
users.append("root")
for user in users:
currently_installed_flatpak = self.flatpak.get_installed(as_user=True, which_user=user)
to_remove_flatpak = self.source.flatpak_packages_to_remove(currently_installed_flatpak, as_user=True, which_user=user)
l.print_list(f"Removing flatpak packages from user installation for user {user}", to_remove_flatpak)
if only_print: continue
self.flatpak.remove(to_remove_flatpak, True, user)
def _upgrade_pkgs(self): def _upgrade_pkgs(self):
""" """
@@ -340,16 +352,12 @@ class Core:
to_install_flatpak = self.source.flatpak_packages_to_install( to_install_flatpak = self.source.flatpak_packages_to_install(
currently_installed_flatpak currently_installed_flatpak
) )
currently_installed_user_flatpak = self.flatpak.get_installed(True)
to_install_user_flatpak = self.source.flatpak_packages_to_install(
currently_installed_user_flatpak, True
)
l.print_list("Installing pacman packages:", to_install_pacman) l.print_list("Installing pacman packages:", to_install_pacman)
if conf.enable_flatpak: if conf.enable_flatpak:
l.print_list("Installing flatpak packages:", to_install_flatpak) l.print_list("Installing flatpak packages:", to_install_flatpak)
l.print_list("Installing user flatpak packages:", to_install_user_flatpak) self._install_user_flatpaks(only_print=True)
# fpm prints a summary so no need to print it twice # fpm prints a summary so no need to print it twice
if self.only_print: if self.only_print:
@@ -362,7 +370,21 @@ class Core:
if conf.enable_flatpak and self.update_flatpaks: if conf.enable_flatpak and self.update_flatpaks:
self.flatpak.install(to_install_flatpak) self.flatpak.install(to_install_flatpak)
self.flatpak.install(to_install_user_flatpak, True) self._install_user_flatpaks()
def _install_user_flatpaks(self, only_print: bool = False):
# get all users through a command instead of pwd because pwd also lists all 'virtual' users. add root since they can also have user installed packages
users = subprocess.run(["users"], check=True, stdout=subprocess.PIPE).stdout.decode().strip().split('\n')
users.append("root")
for user in users:
currently_installed_flatpak = self.flatpak.get_installed(as_user=True, which_user=user)
to_install_flatpak = self.source.flatpak_packages_to_install(currently_installed_flatpak, as_user=True, which_user=user)
l.print_list(f"Installing flatpak packages to user installation for user {user}", to_install_flatpak)
if only_print: continue
self.flatpak.install(to_install_flatpak, True, user)
def _create_and_remove_files(self): def _create_and_remove_files(self):
l.print_summary("Installing files.") l.print_summary("Installing files.")
+58 -34
View File
@@ -7,10 +7,9 @@ import os
import pty import pty
import shutil import shutil
import subprocess import subprocess
import sys
import threading
import time import time
import typing import typing
import pwd
import decman import decman
import decman.config as conf import decman.config as conf
@@ -37,7 +36,6 @@ def print_continuation(msg: str, level: int = SUMMARY):
if level == SUMMARY or conf.debug_output or not conf.quiet_output: if level == SUMMARY or conf.debug_output or not conf.quiet_output:
print(f"{_CONTINUATION_PREFIX}{msg}") print(f"{_CONTINUATION_PREFIX}{msg}")
def print_error(error_msg: str): def print_error(error_msg: str):
""" """
Prints an error message to the user. Prints an error message to the user.
@@ -45,7 +43,6 @@ def print_error(error_msg: str):
print(f"{_DECMAN_MSG_TAG} {_RED_PREFIX}ERROR{_RESET_SUFFIX}: {error_msg}") print(f"{_DECMAN_MSG_TAG} {_RED_PREFIX}ERROR{_RESET_SUFFIX}: {error_msg}")
def print_warning(msg: str): def print_warning(msg: str):
""" """
Prints a warning to the user. Prints a warning to the user.
@@ -53,7 +50,6 @@ def print_warning(msg: str):
print(f"{_DECMAN_MSG_TAG} {_YELLOW_PREFIX}WARNING{_RESET_SUFFIX}: {msg}") print(f"{_DECMAN_MSG_TAG} {_YELLOW_PREFIX}WARNING{_RESET_SUFFIX}: {msg}")
def print_summary(msg: str): def print_summary(msg: str):
""" """
Prints a summary message to the user. Prints a summary message to the user.
@@ -61,7 +57,6 @@ def print_summary(msg: str):
print(f"{_DECMAN_MSG_TAG} {_CYAN_PREFIX}SUMMARY{_RESET_SUFFIX}: {msg}") print(f"{_DECMAN_MSG_TAG} {_CYAN_PREFIX}SUMMARY{_RESET_SUFFIX}: {msg}")
def print_list( def print_list(
msg: str, msg: str,
l: list[str], l: list[str],
@@ -121,7 +116,6 @@ def print_list(
print_continuation("", level=level) print_continuation("", level=level)
def print_info(msg: str): def print_info(msg: str):
""" """
Prints a detailed message to the user if verbose output is not disabled. Prints a detailed message to the user if verbose output is not disabled.
@@ -129,7 +123,6 @@ def print_info(msg: str):
if conf.debug_output or not conf.quiet_output: if conf.debug_output or not conf.quiet_output:
print(f"{_DECMAN_MSG_TAG} INFO: {msg}") print(f"{_DECMAN_MSG_TAG} INFO: {msg}")
def print_debug(msg: str): def print_debug(msg: str):
""" """
Prints a detailed message to the user if debug messages are enabled. Prints a detailed message to the user if debug messages are enabled.
@@ -137,7 +130,6 @@ def print_debug(msg: str):
if conf.debug_output: if conf.debug_output:
print(f"{_DECMAN_MSG_TAG} {_GRAY_PREFIX}DEBUG{_RESET_SUFFIX}: {msg}") print(f"{_DECMAN_MSG_TAG} {_GRAY_PREFIX}DEBUG{_RESET_SUFFIX}: {msg}")
def prompt_number( def prompt_number(
msg: str, min_num: int, max_num: int, default: typing.Optional[int] = None msg: str, min_num: int, max_num: int, default: typing.Optional[int] = None
) -> int: ) -> int:
@@ -420,7 +412,7 @@ class Source:
directories: dict[str, decman.Directory], directories: dict[str, decman.Directory],
modules: set[decman.Module], modules: set[decman.Module],
flatpak_packages: set[str], flatpak_packages: set[str],
flatpak_user_packages: set[str], flatpak_user_packages: set[tuple[str,str]],
ignored_flatpak_packages: set[str], ignored_flatpak_packages: set[str],
): ):
self.pacman_packages = pacman_packages self.pacman_packages = pacman_packages
@@ -646,14 +638,14 @@ class Source:
return result return result
def flatpak_packages_to_install( def flatpak_packages_to_install(
self, currently_installed_packages: list[str], as_user: bool = False self, currently_installed_packages: list[str], as_user: bool = False, which_user: str = ""
) -> list[str]: ) -> list[str]:
""" """
Returns all flatpak packages, that are not installed or ignored Returns all flatpak packages, that are not installed or ignored
""" """
result: list[str] = [] result: list[str] = []
for pkg in self._all_flatpak_packages(as_user): for pkg in self._all_flatpak_packages(as_user, which_user):
if pkg in self.ignored_flatpak_packages: if pkg in self.ignored_flatpak_packages:
continue continue
if pkg not in currently_installed_packages: if pkg not in currently_installed_packages:
@@ -661,7 +653,7 @@ class Source:
return result return result
def flatpak_packages_to_remove( def flatpak_packages_to_remove(
self, currently_installed_packages: list[str], as_user: bool = False self, currently_installed_packages: list[str], as_user: bool = False, which_user: str = ""
) -> list[str]: ) -> list[str]:
""" """
This returns a list of flatpak app ids, that need to be removed since they are installed but not found in either the list of ignored packages, This returns a list of flatpak app ids, that need to be removed since they are installed but not found in either the list of ignored packages,
@@ -671,7 +663,7 @@ class Source:
for package in currently_installed_packages: for package in currently_installed_packages:
if package in self.ignored_flatpak_packages: if package in self.ignored_flatpak_packages:
continue continue
if package not in self._all_flatpak_packages(as_user): if package not in self._all_flatpak_packages(as_user, which_user):
result.append(package) result.append(package)
return result return result
@@ -705,17 +697,28 @@ class Source:
result.update(module.pacman_packages()) result.update(module.pacman_packages())
return result return result
def _all_flatpak_packages(self, as_user: bool = False) -> set[str]: def _all_flatpak_packages(self, as_user: bool = False, which_user: str = "") -> set[str]:
# loop through all the user packages and save which ones are owned by the currently selected user
current_user_flatpak_packages = []
for pkg in self.flatpak_user_packages:
if not pkg[0] == which_user: continue
current_user_flatpak_packages.append(pkg[1])
result = set() result = set()
result.update( result.update(
self.flatpak_packages if not as_user else self.flatpak_user_packages self.flatpak_packages if not as_user else current_user_flatpak_packages
) )
for module in self.modules: for module in self.modules:
if module.enabled: if module.enabled:
module_current_user_flatpak_packages = []
for pkg in module.flatpak_user_packages():
if not pkg[0] == which_user: continue
module_current_user_flatpak_packages.append(pkg[1])
result.update( result.update(
module.flatpak_packages() module.flatpak_packages()
if not as_user if not as_user
else module.flatpak_user_packages() else module_current_user_flatpak_packages
) )
return result return result
@@ -953,7 +956,6 @@ def print_highlighted_pacman_messages(output: str):
# Break, as to not print the same line again if it contains multiple keywords # Break, as to not print the same line again if it contains multiple keywords
break break
def echo_and_capture_command(program: list[str]) -> tuple[int, str]: def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
""" """
Runs the given CLI program and arguments. Runs the given CLI program and arguments.
@@ -973,21 +975,27 @@ def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
return (returncode, output) return (returncode, output)
def get_user_info(username: str) -> tuple[int, int]:
info = pwd.getpwnam(username)
return (info.pw_uid, info.pw_gid)
class Flatpak: class Flatpak:
def __init__(self) -> None: def __init__(self) -> None:
pass pass
def get_installed(self, as_user: bool = False) -> list[str]: def get_installed(self, as_user: bool = False, which_user: str = "") -> list[str]:
""" """
Return all of the installed applications. Dependencies and runtimes are exluded since they will not be explicitly installed and thus flatpak will manage them. Return all of the installed applications. Dependencies and runtimes are exluded since they will not be explicitly installed and thus flatpak will manage them.
""" """
try: try:
uinfo = get_user_info(which_user)
packages = ( packages = (
subprocess.run( subprocess.run(
conf.commands.list_flatpak_pkgs(as_user), conf.commands.list_flatpak_pkgs(as_user),
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
user=uinfo[0] if as_user else 0,
group=uinfo[1] if as_user else 0,
) )
.stdout.decode() .stdout.decode()
.strip() .strip()
@@ -1007,19 +1015,26 @@ class Flatpak:
user_facing_msg=f"Failed to get installed flatpak packages using '{error.cmd}'. Output: {error.stdout}." user_facing_msg=f"Failed to get installed flatpak packages using '{error.cmd}'. Output: {error.stdout}."
) from error ) from error
def install(self, packages: list[str], as_user: bool = False): def install(self, packages: list[str], as_user: bool = False, which_user: str = ""):
""" """
Install the listed flatpak packages. Install the listed flatpak packages.
""" """
if not packages: if not packages:
return return
returncode, _output = echo_and_capture_command( uinfo = get_user_info(which_user)
conf.commands.install_flatpak_pkgs(packages, as_user)
proc = subprocess.run(
conf.commands.install_flatpak_pkgs(packages, as_user),
check=True,
stdout=subprocess.PIPE,
user=uinfo[0] if as_user else 0,
group=uinfo[1] if as_user else 0,
) )
if returncode != 0:
if proc.returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install flatpak packages. Process exited with code {returncode}." f"Failed to install flatpak packages. Process exited with code {proc.returncode}."
) )
def upgrade(self) -> None: def upgrade(self) -> None:
@@ -1032,32 +1047,41 @@ class Flatpak:
f"Failed to upgrade flatpak packages. Process exited with code {returncode}." f"Failed to upgrade flatpak packages. Process exited with code {returncode}."
) )
def remove(self, packages: list[str], as_user: bool = False): def remove(self, packages: list[str], as_user: bool = False, which_user: str = ""):
""" """
Remove all the listed packages and their unused dependecies. This has to happen in two steps. Remove all the listed packages and their unused dependecies. This has to happen in two steps.
""" """
if not packages: if not packages:
return return
returncode, _output = echo_and_capture_command( uinfo = get_user_info(which_user)
conf.commands.remove_flatpak(packages, as_user)
proc = subprocess.run(
conf.commands.remove_flatpak(packages, as_user),
check=True,
stdout=subprocess.PIPE,
user=uinfo[0] if as_user else 0,
group=uinfo[1] if as_user else 0,
) )
if not returncode == 0: if not proc.returncode == 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to remove flatpak packages. Process exited with code {returncode}." f"Failed to remove flatpak packages. Process exited with code {proc.returncode}."
) )
returncode, _output = echo_and_capture_command( proc = subprocess.run(
conf.commands.remove_unused_flatpak() conf.commands.remove_unused_flatpak(as_user),
check=True,
stdout=subprocess.PIPE,
user=uinfo[0] if as_user else 0,
group=uinfo[1] if as_user else 0,
) )
if not returncode == 0: if not proc.returncode == 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to remove unused flatpak packages. Process exited with code {returncode}." f"Failed to remove unused flatpak packages. Process exited with code {proc.returncode}."
) )
class Systemd: class Systemd:
""" """
Interface for interacting with systemd. Interface for interacting with systemd.