23 Commits
Author SHA1 Message Date
Kivi Kaitaniemi a566adf546 Release v0.4.1 2025-12-12 14:37:18 +02:00
Kivi Kaitaniemi 616efa9629 Remove accidentally commited wheel 2025-12-12 14:34:30 +02:00
Kivi Kaitaniemi 409146cf14 Release v0.4.0 2025-12-10 00:34:57 +02:00
Kivi Kaitaniemi 70306ce2ac Merge branch 'gro-david-main' 2025-12-10 00:30:09 +02:00
Kivi Kaitaniemi 2378f49676 Run flatpak commands on users with UID>=1000 2025-12-10 00:29:15 +02:00
Kivi Kaitaniemi a54fad6570 Merge branch 'main' of https://github.com/gro-david/decman into gro-david-main 2025-12-09 11:54:45 +02:00
Dávid Groniewsky 6ae8c78959 Switched the variable type of user defined flatpak packages to match the type of the user systemd services 2025-10-13 09:36:46 +02:00
David Groniewsky 69bcaadf90 Forgot to set the HOME variable so set that as suggested in the change request 2025-10-09 22:47:35 +02:00
David Groniewsky 7ea1ef2d81 Fixed the management of flatpak packages installed to the user installation as requested in the PR 2025-10-09 22:42:19 +02:00
David Groniewsky d765eea3f4 Simple fixes 2025-10-08 19:13:08 +02:00
Kivi Kaitaniemi 37e85a0814 Fix test that occasionally fails due to set ordering being indeterminate 2025-10-02 00:38:13 +03:00
Dávid Groniewsky 7320c88940 Updated the README and the examples to include user wide installation of flatpak packages. 2025-10-01 11:19:04 +02:00
Dávid Groniewsky 613beaa917 Added the option for installing flatpaks to the user installation. 2025-10-01 11:15:26 +02:00
Dávid Groniewsky 515a27cb7d Added the check for missing flatpak installation while the flatpaks are enabled. An error will be printed and decman will exit. 2025-09-30 22:45:54 +02:00
Dávid Groniewsky dbeed0b7d8 Updated the README with the announcement that flatpaks can now be managed. Added the required documentation to the example. 2025-09-30 22:40:19 +02:00
Dávid Groniewsky 9ce42ccf3e Added the config option to disable flatpaks (disabled by default), and the flag --no-flatpaks which will skip flatpak packages. 2025-09-30 22:30:24 +02:00
Dávid Groniewsky 0ecd835090 Extended the flatpak functionality to modules. All works as expected. 2025-09-29 15:19:42 +02:00
Dávid Groniewsky 27a8c279c4 Removed the build and dist directories 2025-09-29 14:58:21 +02:00
Dávid Groniewsky 401233a352 Added support for declaring installed and ignored flatpaks in the source file. For this the variables 'flatpak_packages' and 'ignored_flatpak_packages' are used. These work like the pacman equivalents. There were test cases appended for flatpak testing. I have installed the modified version on my system and tested it extensively. No errors were found. 2025-09-29 14:57:34 +02:00
Kivi Kaitaniemi f3910a6bc9 Release v0.3.4 2025-05-01 17:13:02 +03:00
Kivi Kaitaniemi bce9be5ebc Use pty for capturing and echoing cmd output 2025-05-01 17:10:16 +03:00
Kivi Kaitaniemi ec19b21244 Mention package groups in README.md 2025-05-01 16:49:06 +03:00
Kivi Kaitaniemi 47ea816d3f Update README.md & examples 2025-03-08 19:07:04 +02:00
10 changed files with 568 additions and 130 deletions
+1
View File
@@ -4,3 +4,4 @@ build/
*.egg-info/
venv/
dist/
+29 -7
View File
@@ -1,5 +1,18 @@
# Decman
> 🎉 Early support for Flatpaks was just added! 🎉
> By default flatpak management is disabled. Support is in early stages so expect bugs.
> ```py
> import decman
> import decman.config
> decman.config.enable_flatpak = True
> # You can add system wide packages as well as user packages
> decman.flatpak_packages += ["org.signal.Signal"]
> decman.flatpak_user_packages["user"] = decman.flatpak_user_packages.get("user", [])
> decman.flatpak_user_packages["user"].append("dev.zed.Zed")
> ```
Decman is a declarative package & configuration manager for Arch Linux. It allows you to manage installed packages, your dotfiles, enabled systemd units, and run commands automatically. Your system is configured using python so your configuration can be very adaptive.
If you want, you can also use decman with other configuration languages. See the [example with TOML later in this README](#decman-with-other-configuration-languages).
@@ -111,17 +124,22 @@ decman --help
Clone the decman PKGBUILD:
```sh
git clone https://github.com/kiviktnm/decman-pkgbuild.git
git clone https://aur.archlinux.org/decman.git
```
Review the PKGBUILD and install it.
```sh
cd decman-pkgbuild
cd decman
makepkg -si
```
So far I have not created an AUR package for decman, because I'm not sure if other people would find decman useful.
Remember to add decman to its own configuration.
```py
import decman
decman.aur_packages += ["decman"]
```
## What decman manages?
@@ -129,6 +147,8 @@ So far I have not created an AUR package for decman, because I'm not sure if oth
Decman can be used to install pacman packages. Decman will install all packages defined in the source and **remove** all explicitly installed packages not defined in the source. You don't need to list dependencies as those will be handeled by pacman. You can set packages to be ignored by decman, so that it won't install them nor remove them.
Please keep in mind that decman doesn't play well with package groups, since all packages part of that group will be installed explicitly. After the initial run decman will now try to remove those packages since it only knows that the group itself should be explicitly installed. Instead of package groups, use meta packages.
```py
# Include both foreign and pacman packages here.
decman.ignored_packages += ["yay", "opendoas"]
@@ -145,12 +165,13 @@ Decman can install AUR packages as well as user defined packages. Foreign packag
Here is an example of a user package. Managing user packages is somewhat cumbersome as you have to declare their versions, dependencies and make dependencies manually. However, you probably won't install many user packages anyway.
```py
# Note, decman now has a aur package, I recommend using that instead.
# Also, this example may be out of date
decman.user_packages.append(
decman.UserPackage(
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.3.3",
version="0.4.1",
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
make_dependencies=[
"python-setuptools", "python-build", "python-installer", "python-wheel"
@@ -278,10 +299,11 @@ aur_packages = ["protonvpn"]
enabled_systemd_units = ["NetworkManager.service"]
ignored_packages = ["yay"]
user_packages = [{
# Note, decman now has a aur package, I recommend using that instead.
# Also, this example may be out of date
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.3.3",
version="0.4.1",
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
make_dependencies=[
"python-setuptools", "python-build", "python-installer", "python-wheel"
+22 -9
View File
@@ -1,10 +1,10 @@
# from import is ok for importing classes and functions
# just remember to not import variables this way
from decman import Module, File, Directory, UserPackage, sh, prg
from decman import Directory, File, Module, UserPackage, prg, sh
class MyModule(Module):
def __init__(self):
self.pkgs = ["rust"]
self.update_rustup = False
@@ -28,6 +28,8 @@ class MyModule(Module):
# or run a program with arguments.
prg(["usermod", "--append", "--groups", "mygroup", "kk"])
# NOTE! Removing a enabled module from decman.module means that on_disable will not run.
# Instead disable the module.
def on_disable(self):
# You can run commands as any user
sh("whoami", user="kk")
@@ -58,18 +60,21 @@ class MyModule(Module):
def files(self) -> dict[str, File]:
# Variables are substituted in text files automatically.
return {
"/usr/local/bin/say-hello":
File(content="#!/usr/bin/env bash\necho %msg%", permissions=0o755),
"/usr/local/bin/say-hello": File(
content="#!/usr/bin/env bash\necho %msg%", permissions=0o755
),
# Variables are not substituted in binary files.
"/usr/local/share/say-hello/image.png":
File(source_file="files/i-dont-exist.png", bin_file=True),
"/usr/local/share/say-hello/image.png": File(
source_file="files/i-dont-exist.png", bin_file=True
),
}
def directories(self) -> dict[str, Directory]:
# Directories are handeled the same way. Variables are substituted in text files.
return {
"/home/kk/.config/mod-app/":
Directory(source_directory="files/app-config", owner="kk")
"/home/kk/.config/mod-app/": Directory(
source_directory="files/app-config", owner="kk"
)
}
# Packages and systemd units are basically the same with modules as without modules.
@@ -79,10 +84,12 @@ class MyModule(Module):
return self.pkgs
def user_packages(self) -> list[UserPackage]:
# Note, decman now has a aur package, I recommend using that instead.
# Also, this example may be out of date
return [
UserPackage(
pkgname="decman-git",
version="0.3.3",
version="0.4.1",
provides=["decman"],
dependencies=[
"python",
@@ -106,6 +113,12 @@ class MyModule(Module):
def aur_packages(self) -> list[str]:
return ["protonvpn"]
def flatpak_packages(self) -> list[str]:
return ["org.mozilla.firefox"]
def flatpak_user_packages(self) -> dict[str, list[str]]:
return {"username": ["io.github.kolunmi.Bazaar"]}
def systemd_units(self) -> list[str]:
return ["reflector.timer"]
+22 -5
View File
@@ -1,8 +1,8 @@
# This example covers all decman features and many useful ways of configuring a system.
# Configuration can be as simple or as complex as is needed.
import socket
import os
import socket
# Note: Do NOT use from imports for global variables
# BAD: from decman import packages/modules/etc
@@ -10,7 +10,10 @@ import decman
import decman.config
# This is fine since the thing being imported is a class and not a global variable.
from decman import UserPackage, File, Directory, UserRaisedError
from decman import Directory, File, UserPackage, UserRaisedError
# Flatpaks are disabled by default. This way no already installed flatpaks will get suddenly deleted.
decman.config.enable_flatpak = True
# Configuring what packages are installed is easy.
# Duplicates are OK, so if you have multiple modules that want to ensure a package is installed,
@@ -26,7 +29,19 @@ decman.packages += ["python", "python", "devtools", "git", "networkmanager"]
decman.ignored_packages += ["rustup", "yay"]
# Installing AUR packages is easy.
decman.aur_packages += ["protonvpn"]
decman.aur_packages += ["decman", "protonvpn"]
# Flatpaks work the same way as all other packages.
decman.flatpak_packages += ["dev.qwery.AddWater"]
# They too can be ignored of course
decman.ignored_flatpak_packages += ["org.signal.Signal"]
# You can also install them to your user installation instead of the system installation
# Ensure that previous user installed flatpak declarations aren't overwritten and they are initialized.
decman.flatpak_user_packages["kk"] = decman.flatpak_user_packages.get("kk", [])
# Now add the package.
decman.flatpak_user_packages["kk"].append("dev.zed.Zed")
# To import GPG keys, set the GNUPGHOME environment variable.
# It can easily be done with python as well.
@@ -36,11 +51,13 @@ decman.config.makepkg_user = "kk"
# You can also install packages from anywhere, but then you must include some
# information about the package. The git_url is the url to the PKGBUILD,
# This example may not be up to date, but you should keep these up to date with the PKGBUILD.
#
# Note, decman now has a aur package, I recommend using that instead.
# Also, this example may be out of date
decman.user_packages.append(
UserPackage(
pkgname="decman-git",
version="0.3.3",
version="0.4.1",
provides=["decman"],
dependencies=[
"python",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "decman"
version = "0.3.3"
version = "0.4.1"
description = "Declarative package & configuration manager for Arch Linux."
license = {file = "LICENSE"}
authors = [
+17
View File
@@ -380,6 +380,20 @@ class Module:
"""
return []
def flatpak_packages(self) -> list[str]:
"""
Override this method to return flatpak packages that should be installed to the system installation as a part of this
Module.
"""
return []
def flatpak_user_packages(self) -> dict[str, list[str]]:
"""
Override this method to return flatpak packages that should be installed to the user installation as a part of this
Module.
"""
return {}
def systemd_units(self) -> list[str]:
"""
Override this method to return systemd units that should be enabled as a part of this
@@ -412,3 +426,6 @@ enabled_systemd_user_units: dict[str, list[str]] = {}
files: dict[str, File] = {}
directories: dict[str, Directory] = {}
modules: list[Module] = []
flatpak_packages: list[str] = []
flatpak_user_packages: dict[str, list[str]] = {}
ignored_flatpak_packages: list[str] = []
+148 -4
View File
@@ -1,9 +1,12 @@
# pyright: reportUnusedCallResult=false
"""
Module containing the CLI Application.
"""
import argparse
import os
import pwd
import shutil
import sys
import traceback
@@ -52,6 +55,12 @@ def main():
default=False,
help="don't upgrade foreign packages",
)
parser.add_argument(
"--no-flatpaks",
action="store_true",
default=False,
help="don't upgrade flatpak packages",
)
parser.add_argument(
"--no-files", action="store_true", default=False, help="don't install any files"
)
@@ -177,6 +186,7 @@ def _set_up(store: l.Store, args):
args.print,
not args.no_packages,
not args.no_foreign_packages,
not args.no_flatpaks,
not args.no_files,
not args.no_systemd_units,
not args.no_commands,
@@ -195,6 +205,7 @@ class Core:
self.only_print,
self.update_packages,
self.update_foreign_packages,
self.update_flatpaks,
self.update_files,
self.update_units,
self.run_commands,
@@ -202,9 +213,16 @@ class Core:
self.force_build,
) = opts
if conf.enable_flatpak and not shutil.which("flatpak"):
l.print_error(
"Flatpaks have been enabled in the source file, but the flatpak command could not be found. Either disable flatpaks or make sure that flatpak is installed and can be accessed by decman. Exiting."
)
sys.exit(1)
self.store = store
self.source = _resolve_source()
self.pacman = l.Pacman()
self.flatpak = l.Flatpak()
self.systemctl = l.Systemd(store)
self.fpkg_search = fpm.ExtendedPackageSearch(self.pacman)
@@ -219,6 +237,7 @@ class Core:
"""
Run the main logic of decman.
"""
if self.update_units:
self._disable_units()
@@ -257,37 +276,155 @@ class Core:
self.systemctl.disable_user_units(units, user)
def _remove_pkgs(self):
"""
Remove pacman and flatpak packages
"""
# pacman
currently_installed = self.pacman.get_installed()
to_remove = self.source.packages_to_remove(currently_installed)
l.print_list("Removing packages:", to_remove)
if not self.only_print:
currently_installed_flatpak = self.flatpak.get_installed()
to_remove_flatpak = self.source.flatpak_packages_to_remove(
currently_installed_flatpak
)
l.print_list("Removing pacman packages:", to_remove)
if conf.enable_flatpak and self.update_flatpaks:
l.print_list("Removing flatpak packages:", to_remove_flatpak)
self._remove_user_flatpaks(only_print=True)
if self.only_print:
return
self.pacman.remove(to_remove)
# flatpak
if conf.enable_flatpak and self.update_flatpaks:
self.flatpak.remove(to_remove_flatpak)
self._remove_user_flatpaks()
def _remove_user_flatpaks(self, only_print: bool = False):
# Get all non system users (users that have uid >= 1000), also ignore nobody
users = [
u.pw_name
for u in pwd.getpwall()
if u.pw_uid >= 1000 and u.pw_name not in ("nobody",)
]
# Add root to users
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):
"""
Upgrade pacman, fpm and flatpak packages
"""
# flatpak + fpm
l.print_summary("Upgrading packages.")
if not self.only_print:
if self.only_print:
return
self.pacman.upgrade()
if conf.enable_fpm and self.update_foreign_packages:
self.fpm.upgrade(
self.upgrade_devel, self.force_build, self.source.ignored_packages
)
# flatpak
if conf.enable_flatpak and self.update_flatpaks:
l.print_summary("Upgrading flatpak packages.")
self.flatpak.upgrade()
users = [
u.pw_name
for u in pwd.getpwall()
if u.pw_uid >= 1000 and u.pw_name not in ("nobody",)
]
# Add root to users
users.append("root")
for user in users:
l.print_summary(f"Upgrading flatpak packages for {user}.")
self.flatpak.upgrade(True, user)
def _install_pkgs(self):
"""
Installs all pacman, fpm, and flatpak packages.
"""
# pacman + fpm
currently_installed = self.pacman.get_installed()
to_install_pacman = self.source.pacman_packages_to_install(currently_installed)
to_install_fpm = self.source.foreign_packages_to_install(currently_installed)
# flatpak
currently_installed_flatpak = self.flatpak.get_installed()
to_install_flatpak = self.source.flatpak_packages_to_install(
currently_installed_flatpak
)
l.print_list("Installing pacman packages:", to_install_pacman)
# fpm prints a summary so no need to print it twice
if self.only_print:
l.print_list("Installing foreign packages:", to_install_fpm)
if not self.only_print:
if conf.enable_flatpak and self.update_flatpaks:
l.print_list("Installing flatpak packages:", to_install_flatpak)
if self.only_print:
self._install_user_flatpaks(only_print=True)
return
self.pacman.install(to_install_pacman)
if conf.enable_fpm and self.update_foreign_packages:
self.fpm.install(to_install_fpm, force=self.force_build)
if conf.enable_flatpak and self.update_flatpaks:
self.flatpak.install(to_install_flatpak)
# Print summary before the action
self._install_user_flatpaks(only_print=True)
self._install_user_flatpaks()
def _install_user_flatpaks(self, only_print: bool = False):
# Get all non system users (users that have uid >= 1000), also ignore nobody
users = [
u.pw_name
for u in pwd.getpwall()
if u.pw_uid >= 1000 and u.pw_name not in ("nobody",)
]
# Add root to users
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
)
if only_print:
l.print_list(
f"Installing flatpak packages to user installation for user {user}",
to_install_flatpak,
)
continue
self.flatpak.install(to_install_flatpak, True, user)
def _create_and_remove_files(self):
l.print_summary("Installing files.")
@@ -346,6 +483,10 @@ def _resolve_source() -> l.Source:
for user, units in decman.enabled_systemd_user_units.items():
enabled_systemd_user_units[user] = set(units)
flatpak_user_packages = {}
for user, pkgs in decman.flatpak_user_packages.items():
flatpak_user_packages[user] = set(pkgs)
return l.Source(
pacman_packages=set(decman.packages),
aur_packages=set(decman.aur_packages),
@@ -356,6 +497,9 @@ def _resolve_source() -> l.Source:
files=decman.files,
directories=decman.directories,
modules=set(decman.modules),
flatpak_packages=set(decman.flatpak_packages),
flatpak_user_packages=flatpak_user_packages,
ignored_flatpak_packages=set(decman.ignored_flatpak_packages),
)
+58
View File
@@ -34,6 +34,20 @@ class Commands:
"""
return ["pacman", "-Qeq", "--color=never"]
def list_flatpak_pkgs(self, as_user: bool = False) -> list[str]:
"""
Running this command outputs a newline separated list of installed flatpak application ids
The first line just says 'Application ID' so this one is ignored.
"""
return [
"flatpak",
"list",
"--app",
"--user" if as_user else "--system",
"--columns",
"application",
]
def list_foreign_pkgs_versioned(self) -> list[str]:
"""
Running this command outputs a newline seperated list of installed packages and their
@@ -47,6 +61,12 @@ class Commands:
"""
return ["pacman", "-S", "--color=always", "--needed"] + pkgs
def install_flatpak_pkgs(self, pkgs: list[str], as_user: bool = False) -> list[str]:
"""
Running this command installs all listed packages, and their dependencies/runtimes automatically.
"""
return ["flatpak", "install", "-y", "--user" if as_user else "--system"] + pkgs
def install_files(self, pkg_files: list[str]) -> list[str]:
"""
Running this command installs the given packages files.
@@ -78,6 +98,18 @@ class Commands:
"""
return ["pacman", "-Syu", "--color=always"]
def upgrade_flatpak(self, as_user: bool = False) -> list[str]:
"""
Updates all installed flatpak REFs including runtimes and dependencies.
"""
return [
"flatpak",
"update",
"--noninteractive",
"-y",
"--user" if as_user else "--system",
]
def remove(self, pkgs: list[str]) -> list[str]:
"""
Running this command removes the given packages and their dependencies
@@ -85,6 +117,31 @@ class Commands:
"""
return ["pacman", "-Rs", "--color=always"] + pkgs
def remove_flatpak(self, pkgs: list[str], as_user: bool = False) -> list[str]:
"""
Running this command will remove the listed REFs. Unused dependencies might be kept, but to remove them another command needs to be run.
"""
return [
"flatpak",
"remove",
"--noninteractive",
"-y",
"--user" if as_user else "--system",
] + pkgs
def remove_unused_flatpak(self, as_user: bool = False) -> list[str]:
"""
This will remove all unused flatpak dependencies and runtimes.
"""
return [
"flatpak",
"remove",
"--noninteractive",
"-y",
"--unused",
"--user" if as_user else "--system",
]
def enable_units(self, units: list[str]) -> list[str]:
"""
Running this command enables the given systemd units.
@@ -234,4 +291,5 @@ build_dir: str = "/tmp/decman/build"
pkg_cache_dir: str = "/var/cache/decman"
aur_rpc_timeout: typing.Optional[int] = 30
enable_fpm: bool = True
enable_flatpak: bool = False
number_of_packages_stored_in_cache: int = 3
+219 -44
View File
@@ -4,10 +4,10 @@ Library module for decman.
import json
import os
import pty
import pwd
import shutil
import subprocess
import sys
import threading
import time
import typing
@@ -418,6 +418,9 @@ class Source:
files: dict[str, decman.File],
directories: dict[str, decman.Directory],
modules: set[decman.Module],
flatpak_packages: set[str],
flatpak_user_packages: dict[str, set[str]],
ignored_flatpak_packages: set[str],
):
self.pacman_packages = pacman_packages
self.aur_packages = aur_packages
@@ -428,6 +431,9 @@ class Source:
self.files = files
self.directories = directories
self.modules = modules
self.flatpak_packages = flatpak_packages
self.flatpak_user_packages = flatpak_user_packages
self.ignored_flatpak_packages = ignored_flatpak_packages
def run_on_enable(self, store: Store):
"""
@@ -638,6 +644,43 @@ class Source:
result.append(pkg)
return result
def flatpak_packages_to_install(
self,
currently_installed_packages: list[str],
as_user: bool = False,
which_user: str = "",
) -> list[str]:
"""
Returns all flatpak packages, that are not installed or ignored
"""
result: list[str] = []
for pkg in self._all_flatpak_packages(as_user, which_user):
if pkg in self.ignored_flatpak_packages:
continue
if pkg not in currently_installed_packages:
result.append(pkg)
return result
def flatpak_packages_to_remove(
self,
currently_installed_packages: list[str],
as_user: bool = False,
which_user: 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,
the list of system packages or the list of user packages that need to be installed.
"""
result: list[str] = []
for package in currently_installed_packages:
if package in self.ignored_flatpak_packages:
continue
if package not in self._all_flatpak_packages(as_user, which_user):
result.append(package)
return result
def all_enabled_modules(self) -> list[tuple[str, str]]:
"""
Returns all enabled modules and their versions.
@@ -667,6 +710,27 @@ class Source:
result.update(module.pacman_packages())
return result
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 = self.flatpak_user_packages.get(which_user, [])
result = set()
result.update(
self.flatpak_packages if not as_user else current_user_flatpak_packages
)
for module in self.modules:
if not module.enabled:
continue
result.update(
module.flatpak_packages()
if not as_user
else module.flatpak_user_packages().get(which_user, [])
)
return result
def _all_foreign_pkgs(self) -> set[str]:
result = set()
result.update(self.aur_packages)
@@ -907,55 +971,166 @@ def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
Returns a tuple containing the return code of the program as well as all output of the program.
"""
with subprocess.Popen(
program, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
) as process:
os.set_blocking(process.stdout.fileno(), False)
output_thread = _OutputCapturingThread(process.stdout)
output_thread.start()
output = ""
os.set_blocking(sys.stdin.fileno(), False)
def read(fd):
nonlocal output
buffer = os.read(fd, 1024)
output += buffer.decode(encoding="utf-8")
return buffer
# Capture stdin and forward it to the process in a non-blocking manner
while process.poll() is None:
inp = sys.stdin.readline()
returncode = os.waitstatus_to_exitcode(pty.spawn(program, read))
if inp:
process.stdin.write(inp.encode())
process.stdin.flush()
time.sleep(0.1)
os.set_blocking(sys.stdin.fileno(), True)
output_thread.done = True
output_thread.join()
# Capture any output that may not have been yet captured
output = output_thread.output
missing_output = process.stdout.read()
if missing_output:
output += missing_output.decode()
return (process.returncode, output)
return (returncode, output)
class _OutputCapturingThread(threading.Thread):
def __init__(self, stream):
super().__init__()
self._stream = stream
self.output = ""
self.done = False
def get_user_info(username: str) -> tuple[int, int]:
info = pwd.getpwnam(username)
return (info.pw_uid, info.pw_gid)
def run(self):
while not self.done and not self._stream.closed:
output = self._stream.read(1000)
if output:
output = output.decode()
self.output += output
print(output, end="", flush=True)
time.sleep(0.1)
class Flatpak:
def __init__(self) -> None:
pass
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.
"""
try:
uinfo: tuple[int, int] = (0, 0)
env = os.environ.copy()
user_env = env.copy()
user_env["HOME"] = os.path.expanduser(f"~{which_user}")
if as_user:
uinfo = get_user_info(which_user)
proc = subprocess.run(
conf.commands.list_flatpak_pkgs(as_user),
check=True,
stdout=subprocess.PIPE,
user=uinfo[0],
group=uinfo[1],
env=user_env if as_user else env,
)
packages = proc.stdout.decode().strip().split("\n")
# print(
# f"as_user: {as_user}, which_user: {which_user}, uinfo: {uinfo}, stdout: {proc.stdout.decode()}, packages: {packages}"
# )
# The header might be included. It might also not. This will make sure that it is not present.
if "Application ID" in packages:
packages.remove("Application ID")
if packages == [""]:
return []
return packages
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
user_facing_msg=f"Failed to get installed flatpak packages using '{error.cmd}'. Output: {error.stdout}."
) from error
def install(
self, packages: list[str], as_user: bool = False, which_user: str = "root"
):
"""
Install the listed flatpak packages.
"""
if not packages:
return
uinfo: tuple[int, int] = (0, 0)
if as_user:
uinfo = get_user_info(which_user)
env = os.environ.copy()
user_env = env.copy()
user_env["HOME"] = os.path.expanduser(f"~{which_user}")
proc = subprocess.run(
conf.commands.install_flatpak_pkgs(packages, as_user),
check=True,
user=uinfo[0],
group=uinfo[1],
env=user_env if as_user else env,
)
if proc.returncode != 0:
raise err.UserFacingError(
f"Failed to install flatpak packages. Process exited with code {proc.returncode}."
)
def upgrade(self, as_user: bool = False, which_user: str = "root") -> None:
"""
Upgrade all flatpak packages.
"""
uinfo: tuple[int, int] = (0, 0)
if as_user:
uinfo = get_user_info(which_user)
env = os.environ.copy()
user_env = env.copy()
user_env["HOME"] = os.path.expanduser(f"~{which_user}")
proc = subprocess.run(
conf.commands.upgrade_flatpak(as_user=True),
check=True,
user=uinfo[0],
group=uinfo[1],
env=user_env if as_user else env,
)
if not proc.returncode == 0:
raise err.UserFacingError(
f"Failed to upgrade flatpak packages. Process exited with code {proc.returncode}."
)
def remove(
self, packages: list[str], as_user: bool = False, which_user: str = "root"
):
"""
Remove all the listed packages and their unused dependecies. This has to happen in two steps.
"""
if not packages:
return
uinfo: tuple[int, int] = (0, 0)
if as_user:
uinfo = get_user_info(which_user)
env = os.environ.copy()
user_env = env.copy()
user_env["HOME"] = os.path.expanduser(f"~{which_user}")
proc = subprocess.run(
conf.commands.remove_flatpak(packages, as_user),
check=True,
user=uinfo[0],
group=uinfo[1],
env=user_env if as_user else env,
)
if not proc.returncode == 0:
raise err.UserFacingError(
f"Failed to remove flatpak packages. Process exited with code {proc.returncode}."
)
proc = subprocess.run(
conf.commands.remove_unused_flatpak(as_user),
check=True,
user=uinfo[0] if as_user else 0,
group=uinfo[1] if as_user else 0,
env=user_env if as_user else env,
)
if not proc.returncode == 0:
raise err.UserFacingError(
f"Failed to remove unused flatpak packages. Process exited with code {proc.returncode}."
)
class Systemd:
+33 -42
View File
@@ -1,12 +1,13 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
from typing import override
import unittest
from decman import Module, UserPackage
from decman.lib import Source, Store
from decman import UserPackage, Module
class ExistingTestModule(Module):
def __init__(self):
self.on_enable_executed = False
self.on_disable_executed = False
@@ -28,7 +29,6 @@ class ExistingTestModule(Module):
class ExistingChangedVersionTestModule(Module):
def __init__(self):
self.on_enable_executed = False
self.on_disable_executed = False
@@ -50,7 +50,6 @@ class ExistingChangedVersionTestModule(Module):
class EnabledTestModule(Module):
def __init__(self):
self.on_enable_executed = False
self.on_disable_executed = False
@@ -76,9 +75,11 @@ class EnabledTestModule(Module):
def systemd_user_units(self) -> dict[str, list[str]]:
return {"muser": ["M_u1.service"]}
def flatpak_packages(self) -> list[str]:
return ["M_f1", "M_f2"]
class DisabledTestModule(Module):
def __init__(self):
self.on_enable_executed = False
self.on_disable_executed = False
@@ -106,7 +107,6 @@ class DisabledTestModule(Module):
class TestSource(unittest.TestCase):
def setUp(self):
self.disabled_module = DisabledTestModule()
self.enabled_module = EnabledTestModule()
@@ -133,7 +133,7 @@ class TestSource(unittest.TestCase):
version="1",
dependencies=["d2"],
git_url="/am/url/yes",
)
),
},
ignored_packages={"i1", "i2"},
systemd_units={"1.service", "2.timer"},
@@ -141,11 +141,13 @@ class TestSource(unittest.TestCase):
modules=modules,
files={},
directories={},
flatpak_packages={"f1", "f2", "f3"},
flatpak_user_packages={"fu1", "fu2", "fu3"},
ignored_flatpak_packages={"i1", "i2"},
)
store = Store()
store.enabled_systemd_units.extend(
["1.service", "3.service", "M_1.service"])
store.enabled_systemd_units.extend(["1.service", "3.service", "M_1.service"])
store.add_enabled_user_systemd_unit("user", "u1.service")
store.add_enabled_user_systemd_unit("user", "u3.service")
store.enabled_modules = {
@@ -174,16 +176,19 @@ class TestSource(unittest.TestCase):
self.currently_installed_packages = currently_installed_packages
def test_all_enabled_modules(self):
enabled_modules = [("Enabled", "1"), ("Existing", "1"),
("ExistingChanged", "2")]
self.assertCountEqual(self.source.all_enabled_modules(),
enabled_modules)
enabled_modules = [
("Enabled", "1"),
("Existing", "1"),
("ExistingChanged", "2"),
]
self.assertCountEqual(self.source.all_enabled_modules(), enabled_modules)
def test_files_to_remove(self):
created_files = ["/test/file1", "/test/file4"]
self.assertCountEqual(
self.source.files_to_remove(self.store, created_files),
["/test/file2", "/test/file3"])
["/test/file2", "/test/file3"],
)
def test_after_update_executed(self):
self.source.run_after_update()
@@ -197,8 +202,7 @@ class TestSource(unittest.TestCase):
self.source.run_after_version_change(self.store)
self.assertTrue(self.enabled_module.after_version_change_executed)
self.assertTrue(
self.existing_module_changed.after_version_change_executed)
self.assertTrue(self.existing_module_changed.after_version_change_executed)
self.assertFalse(self.existing_module.after_version_change_executed)
self.assertFalse(self.disabled_module.after_version_change_executed)
@@ -233,10 +237,7 @@ class TestSource(unittest.TestCase):
def test_user_units_to_enable(self):
self.assertDictEqual(
self.source.user_units_to_enable(self.store),
{
"user": ["u2.timer"],
"muser": ["M_u1.service"]
},
{"user": ["u2.timer"], "muser": ["M_u1.service"]},
)
def test_user_units_to_disable(self):
@@ -247,15 +248,13 @@ class TestSource(unittest.TestCase):
def test_pacman_packages_to_install(self):
self.assertCountEqual(
self.source.pacman_packages_to_install(
self.currently_installed_packages),
self.source.pacman_packages_to_install(self.currently_installed_packages),
["p3", "M_p1", "M_p2"],
)
def test_foreign_packages_to_install(self):
self.assertCountEqual(
self.source.foreign_packages_to_install(
self.currently_installed_packages),
self.source.foreign_packages_to_install(self.currently_installed_packages),
["A1", "U2"],
)
@@ -265,27 +264,21 @@ class TestSource(unittest.TestCase):
["p4", "A4", "M_A1", "M_A2"],
)
class TestModuleUserServices(unittest.TestCase):
class ModuleWithUserServiceOne(Module):
def __init__(self):
super().__init__("one", True, "0")
def systemd_user_units(self) -> dict[str, list[str]]:
return {
"user": ['foo.service']
}
return {"user": ["foo.service"]}
class ModuleWithUserServiceTwo(Module):
def __init__(self):
super().__init__("two", True, "0")
def systemd_user_units(self) -> dict[str, list[str]]:
return {
"user": ['bar.service']
}
return {"user": ["bar.service"]}
def setUp(self) -> None:
self.source = Source(
@@ -297,16 +290,14 @@ class TestModuleUserServices(unittest.TestCase):
systemd_user_units={},
files={},
directories={},
modules={
self.ModuleWithUserServiceOne(),
self.ModuleWithUserServiceTwo()
},
modules={self.ModuleWithUserServiceOne(), self.ModuleWithUserServiceTwo()},
flatpak_packages=set(),
flatpak_user_packages=set(),
ignored_flatpak_packages=set(),
)
self.store = Store()
def test_user_units_to_enable(self):
self.assertDictEqual(
self.source.user_units_to_enable(self.store),
{"user": ["foo.service", "bar.service"]},
)
result = self.source.user_units_to_enable(self.store)
self.assertEqual(len(result), 1)
self.assertCountEqual(result["user"], ["foo.service", "bar.service"])