mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
Start writing documentation for changes
This commit is contained in:
@@ -1,25 +1,18 @@
|
||||
# Decman
|
||||
|
||||
> 🎉 Early support for Flatpaks was just added! 🎉
|
||||
> By default flatpak management is disabled. Support is in early stages so expect bugs.
|
||||
> There are going to be breaking changes!
|
||||
> Decman has undergone an architecture rewrite! The new architecture makes decman more expandable and maintainable.
|
||||
>
|
||||
> The AUR package will receive this update after I have tested it enough.
|
||||
> See this [tag](https://github.com/kiviktnm/decman/tree/0.4.2) for the current version of decman available in the AUR.
|
||||
>
|
||||
> Migration guide is [here](/docs/migrate-to-v1.md).
|
||||
|
||||
> ```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).
|
||||
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.
|
||||
|
||||
## Overview
|
||||
|
||||
A complete example is available in the `example`-directory of this repository. It also serves as documentation so reading it is recommended.
|
||||
[See the complete documentation for using decman.](/docs/README.md)
|
||||
|
||||
To use decman, you need a source file that declares your system installation. I recommend you put this file in source control, for example in a git repository.
|
||||
|
||||
@@ -27,26 +20,28 @@ To use decman, you need a source file that declares your system installation. I
|
||||
|
||||
```py
|
||||
import decman
|
||||
|
||||
from decman import File, Directory
|
||||
|
||||
# Declare installed packages
|
||||
decman.packages += ["python", "git", "networkmanager", "ufw", "neovim"]
|
||||
# Declare installed pacman packages
|
||||
decman.pacman.packages |= {"base", "linux", "linux-firmware", "networkmanager", "ufw", "neovim"}
|
||||
|
||||
# Declare installed aur packages
|
||||
decman.aur_packages += ["protonvpn"]
|
||||
decman.aur.packages |= {"decman"}
|
||||
|
||||
# Declare configuration files
|
||||
# Inline
|
||||
decman.files["/etc/vconsole.conf"] = File(content="KEYMAP=us")
|
||||
# From files within your repository
|
||||
|
||||
# From files within your source repository
|
||||
# (full path here would be /home/user/config/dotfiles/pacman.conf)
|
||||
decman.files["/etc/pacman.conf"] = File(source_file="./dotfiles/pacman.conf")
|
||||
|
||||
# Declare a whole directory
|
||||
decman.directories["/home/user/.config/nvim"] = Directory(source_directory="./dotfiles/nvim",
|
||||
owner="user")
|
||||
|
||||
# Ensure that a systemd unit is enabled.
|
||||
decman.enabled_systemd_units += ["NetworkManager.service"]
|
||||
decman.systemd.enabled_units |= {"NetworkManager.service"}
|
||||
```
|
||||
|
||||
To better organize your system configuration, you can create modules.
|
||||
@@ -54,17 +49,20 @@ To better organize your system configuration, you can create modules.
|
||||
`/home/user/config/syncthing.py`:
|
||||
|
||||
```py
|
||||
from decman import Module, prg
|
||||
from decman import Module, Store, prg
|
||||
from decman.plugins import pacman, systemd
|
||||
|
||||
# Your custom modules are child classes of the module class.
|
||||
# They can override methods of the Module-class.
|
||||
class Syncthing(Module):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name="syncthing", enabled=True, version="1")
|
||||
super().__init__(name="syncthing")
|
||||
|
||||
def on_enable(self):
|
||||
# Run code when a module is first enabled
|
||||
# Run code when a module is first enabled
|
||||
def on_enable(self, store: Store):
|
||||
# Note: store is a key-value store that will persist between decman runs.
|
||||
# You can use it to store your own data as well. Here it is not needed.
|
||||
|
||||
# Call a program
|
||||
prg(["ufw", "allow", "syncthing"])
|
||||
@@ -72,17 +70,26 @@ class Syncthing(Module):
|
||||
# Run any python code
|
||||
print("Remember to setup syncthing with the browser UI!")
|
||||
|
||||
def on_disable(self):
|
||||
# On disable is a special method, it will get executed when this module no longer exists.
|
||||
# Therefore it must be static, take no parameters, and inline all imports.
|
||||
# Imported modules should be available everywhere.
|
||||
@staticmethod
|
||||
def on_disable():
|
||||
# Run code when a module is disabled
|
||||
prg(["ufw", "deny", "syncthing"])
|
||||
import decman
|
||||
decman.prg(["ufw", "deny", "syncthing"])
|
||||
|
||||
def pacman_packages(self) -> list[str]:
|
||||
# Packages part of this module
|
||||
return ["syncthing"]
|
||||
# Decorate a function with @pacman.packages to indicate it returns a set of pacman packages
|
||||
# to be installed
|
||||
@pacman.packages
|
||||
def pacman_packages(self) -> set[str]:
|
||||
return {"syncthing"}
|
||||
|
||||
def systemd_user_units(self) -> dict[str, list[str]]:
|
||||
# Systemd units are declared in a similiar fashion
|
||||
@systemd.user_units
|
||||
def systemd_user_units(self) -> dict[str, set[str]]:
|
||||
# Systemd user units part of this module
|
||||
return {"user": ["syncthing.service"]}
|
||||
return {"user": {"syncthing.service"}}
|
||||
```
|
||||
|
||||
Then import your module in your main source file.
|
||||
@@ -93,12 +100,10 @@ Then import your module in your main source file.
|
||||
import decman
|
||||
from syncthing import Syncthing
|
||||
|
||||
# NOTE! Removing a enabled module from decman.module means that on_disable will not run.
|
||||
# Instead disable the module.
|
||||
decman.modules += [Syncthing()]
|
||||
decman.modules |= {Syncthing()}
|
||||
```
|
||||
|
||||
Then run decman. Note that terminal colors cannot be disabled for decman.
|
||||
Then run decman.
|
||||
|
||||
> [!WARNING]
|
||||
> Decman runs as root. This means that your `source.py` will be executed as root as well.
|
||||
@@ -138,52 +143,62 @@ Remember to add decman to its own configuration.
|
||||
|
||||
```py
|
||||
import decman
|
||||
decman.aur_packages += ["decman"]
|
||||
decman.aur.packages |= {"decman"}
|
||||
```
|
||||
|
||||
## What decman manages?
|
||||
|
||||
### Packages
|
||||
Decman has built-in functionality for managing files and directories. Additionally decman manages system state using plugins. By default decman ships with the following plugins:
|
||||
|
||||
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.
|
||||
- [pacman](/docs/pacman.md)
|
||||
- [systemd](/docs/systemd.md)
|
||||
- [aur](/docs/aur.md)
|
||||
- [flatpak](/docs/flatpak.md)
|
||||
|
||||
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.
|
||||
Plugins can be disabled if desired and flatpaks are disabled by default.
|
||||
|
||||
Please read the documentation to understand the functionality of those plugins in detail. Here are quick examples to show what the default plugins are capable of.
|
||||
|
||||
### Pacman
|
||||
|
||||
Pacman plugins manages native packages. Native packages can be installed from the pacman repositories. This plugin will never touch AUR packages.
|
||||
|
||||
```py
|
||||
# Include both foreign and pacman packages here.
|
||||
decman.ignored_packages += ["yay", "opendoas"]
|
||||
import decman
|
||||
|
||||
# Packages that decman ensures are installed to the system
|
||||
decman.pacman.packages |= {"firefox", "reflector"}
|
||||
|
||||
# These packages will never get installed or removed by decman.
|
||||
decman.pacman.ignored_packages |= {"opendoas"}
|
||||
```
|
||||
|
||||
### Foreign packages
|
||||
### AUR
|
||||
|
||||
> [!NOTE]
|
||||
> Building of foreign packages is not the primary function of decman. There are some issues that I may or may not fix.
|
||||
> If you can't build a package using decman, consider adding it to `ignored_packages` and building it yourself.
|
||||
> Building of AUR or custom packages is not the primary function of decman. There are some issues that I may or may not fix.
|
||||
> If you can't build a package using decman, consider adding it to `decman.aur.ignored_packages` and building it yourself.
|
||||
|
||||
Decman can install AUR packages as well as user defined packages. Foreign packages are AUR and user packages combined.
|
||||
|
||||
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.
|
||||
AUR plugins manages foreign packages. Foreign packages are installed from the AUR or other sources. This plugin will never touch native packages.
|
||||
|
||||
```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",
|
||||
provides=["decman"],
|
||||
version="0.4.1",
|
||||
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
|
||||
make_dependencies=[
|
||||
"python-setuptools", "python-build", "python-installer", "python-wheel"
|
||||
],
|
||||
git_url="https://github.com/kiviktnm/decman-pkgbuild.git",
|
||||
))
|
||||
import decman
|
||||
from decman.plugins.aur import CustomPackage
|
||||
|
||||
# AUR Packages that decman ensures are installed to the system
|
||||
decman.aur.packages |= {"android-studio", "fnm-bin"}
|
||||
|
||||
# These foreign packages will never get installed or removed by decman.
|
||||
decman.aur.ignored_packages |= {"yay"}
|
||||
|
||||
# You can add packages from custom sources.
|
||||
# Just add a package name and repository / directory containing a PKGBUILD
|
||||
decman.aur.custom_packages |= {
|
||||
CustomPackage("decman", git_url="https://github.com/kiviktnm/decman-pkgbuild.git"),
|
||||
CustomPackage("my-own-package", pkgbuild_directory="/path/to/directory/"),
|
||||
}
|
||||
```
|
||||
|
||||
Building of foreign packages happens in a chroot. This creates some overhead, but ensures clean builds. By default the chroot is created to `/tmp/decman/build`. I recommend to use a tmpfs for the `/tmp/` directory to speed up builds. Also make sure that the tmpfs-partition is large enough. I recommend at least 6 GB.
|
||||
|
||||
Build packages are stored in a cache `/var/cache/decman`. By default decman keeps 3 most recent versions of all packages.
|
||||
|
||||
### Systemd units
|
||||
|
||||
> [!NOTE]
|
||||
@@ -191,142 +206,72 @@ Build packages are stored in a cache `/var/cache/decman`. By default decman keep
|
||||
|
||||
Decman can enable systemd services, system wide or for a specific user. Decman will enable all units defined in the source, and disable them when they are removed from the source. If a unit is not defined in the source, decman will not touch it.
|
||||
|
||||
### Files
|
||||
|
||||
Decman functions as a dotfile manager. It will install the defined files and directories to their destinations. You can set file permissions, owners as well as define variables that will be substituted in the installed files. Decman keeps track of all files it creates and when a file is no longer present in your source, it will be also removed from its destination. This helps with keeping your system clean. However, decman won't ever remove directories as they might contain files that weren't created by decman.
|
||||
|
||||
### Commands
|
||||
|
||||
Modules have 4 methods: `on_enable`, `on_disable`, `after_update` and `after_version_change`. These will be executed if the module is enabled, the module is disabled, after every update and after the version of the module has changed. You can use the helper functions `prg` and `sh` to run programs. These programs could for example be used to update packages managed by another package manager.
|
||||
|
||||
## Order of operations
|
||||
|
||||
When decman runs, it does the following things in this order.
|
||||
|
||||
1. Disable systemd units that are no longer in the source.
|
||||
1. Create and update files.
|
||||
1. Remove files no longer in the source.
|
||||
1. Remove packages not defined in the source.
|
||||
1. Upgrade packages.
|
||||
- To upgrade foreign devel packages (eg. `*-git`) use the `--upgrade-devel` CLI option.
|
||||
1. Install new packages.
|
||||
1. Enable new systemd units.
|
||||
1. Run commands:
|
||||
1. `on_enable`
|
||||
1. `after_version_change`
|
||||
1. `on_disable`
|
||||
1. `after_update`
|
||||
|
||||
Operations may be skipped with command line options.
|
||||
|
||||
## Decman with other configuration languages
|
||||
|
||||
Since decman uses Python files to declare your system, you can easily parse another configuration language in your Python source instead of declaring things directly in Python.
|
||||
|
||||
<details>
|
||||
<summary>Here is a basic example using TOML.</summary>
|
||||
|
||||
To use TOML with Python, you need the `toml`-package. To install it in Arch Linux, install the `python-toml`-package.
|
||||
|
||||
This example doesn't allow you to use decman's modules or set decman's settings using TOML. You'll have to set them using Python. With this example you can use both TOML and Python if you want.
|
||||
It would be possible to add support for TOML decman modules, but I don't think it would be worth the effort.
|
||||
|
||||
Write this in your decman source.
|
||||
|
||||
```py
|
||||
import toml
|
||||
import decman
|
||||
|
||||
TOML_CONFIG_FILE="/your/file/here.toml"
|
||||
# System-wide units
|
||||
decman.systemd.enabled_units |= {"NetworkManager.service"}
|
||||
|
||||
# These functions convert TOML tables to decman Files/Directories/UserPackages.
|
||||
|
||||
def toml_to_decman_file(toml_dict) -> decman.File:
|
||||
return decman.File(content=toml_dict.get("content"),
|
||||
source_file=toml_dict.get("source_file"),
|
||||
bin_file=toml_dict.get("bin_file", False),
|
||||
encoding=toml_dict.get("encoding", "utf-8"),
|
||||
owner=toml_dict.get("owner"),
|
||||
group=toml_dict.get("group"),
|
||||
permissions=toml_dict.get("permissions", 0o644))
|
||||
|
||||
|
||||
def toml_to_decman_directory(toml_dict) -> decman.Directory:
|
||||
return decman.Directory(source_directory=toml_dict["source_directory"],
|
||||
bin_files=toml_dict.get("bin_files", False),
|
||||
encoding=toml_dict.get("encoding", "utf-8"),
|
||||
owner=toml_dict.get("owner"),
|
||||
group=toml_dict.get("group"),
|
||||
permissions=toml_dict.get("permissions", 0o644))
|
||||
|
||||
def toml_to_decman_user_package(toml_dict) -> decman.UserPackage:
|
||||
return decman.UserPackage(pkgname=toml_dict["pkgname"],
|
||||
version=toml_dict["version"],
|
||||
dependencies=toml_dict["dependencies"],
|
||||
git_url=toml_dict["git_url"],
|
||||
pkgbase=toml_dict.get("pkgbase"),
|
||||
provides=toml_dict.get("provides"),
|
||||
make_dependencies=toml_dict.get("make_dependencies"),
|
||||
check_dependencies=toml_dict.get("check_dependencies"))
|
||||
|
||||
|
||||
# Parse TOML into a Python dictionary
|
||||
toml_source = toml.load(TOML_CONFIG_FILE)
|
||||
|
||||
# Set decman variables using the parsed dictionary.
|
||||
|
||||
decman.packages = toml_source.get("packages", [])
|
||||
decman.aur_packages = toml_source.get("aur_packages", [])
|
||||
decman.ignored_packages = toml_source.get("ignored_packages", [])
|
||||
decman.enabled_systemd_units = toml_source.get("enabled_systemd_units", [])
|
||||
decman.enabled_systemd_user_units = toml_source.get("enabled_systemd_user_units", {})
|
||||
|
||||
for filename, toml_file_dec in toml_source.get("files", {}).items():
|
||||
decman.files[filename] = toml_to_decman_file(toml_file_dec)
|
||||
|
||||
for dirname, toml_dir_dec in toml_source.get("directories", {}).items():
|
||||
decman.directories[dirname] = toml_to_decman_directory(toml_dir_dec)
|
||||
|
||||
for toml_user_package_dec in toml_source.get("user_packages", []):
|
||||
decman.user_packages.append(toml_to_decman_user_package(toml_user_package_dec))
|
||||
# User specific units
|
||||
decman.systemd.enabled_user_units.setdefault("user", {}).update({"syncthing.service"})
|
||||
```
|
||||
|
||||
Then you can use TOML configuration like this:
|
||||
### Flatpak
|
||||
|
||||
```toml
|
||||
packages = ["python", "git", "networkmanager", "ufw", "neovim", "python-toml"]
|
||||
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",
|
||||
provides=["decman"],
|
||||
version="0.4.1",
|
||||
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
|
||||
make_dependencies=[
|
||||
"python-setuptools", "python-build", "python-installer", "python-wheel"
|
||||
],
|
||||
git_url="https://github.com/kiviktnm/decman-pkgbuild.git"
|
||||
}]
|
||||
```py
|
||||
import decman
|
||||
|
||||
[files]
|
||||
'/etc/vconsole.conf' = { content="KEYMAP=us" }
|
||||
'/etc/pacman.conf' = { source_file="./dotfiles/pacman.conf" }
|
||||
# Flatpaks that decman ensures are installed to the system
|
||||
decman.flatpak.packages |= {"org.mozilla.firefox", "org.signal.Signal"}
|
||||
|
||||
[directories]
|
||||
'/home/user/.config/nvim' = { source_directory="./dotfiles/nvim", owner="user" }
|
||||
# Flatpaks can be installed to specific users only
|
||||
decman.flatpak.user_packages.setdefault("user", {}).update({"com.valvesoftware.Steam"})
|
||||
|
||||
# To set other file/directory attributes, just add them like this. Note that here I am not using octal notation for permissions.
|
||||
'/home/user/.config/example' = { source_directory="./dotfiles/example", owner="user", group="user", bin_files=true, encoding="utf-8", permissions=448 }
|
||||
|
||||
[enabled_systemd_user_units]
|
||||
user = ["syncthing.service"]
|
||||
user2 = ["example.service", "another.service"]
|
||||
# These flatpaks will never get installed or removed by decman.
|
||||
decman.flatpak.ignored_packages |= {"dev.zed.Zed"}
|
||||
```
|
||||
|
||||
</details>
|
||||
## Managing plugins and the order of operations
|
||||
|
||||
The order of operations is managed by setting `decman.execution_order`. This is also the default.
|
||||
|
||||
```py
|
||||
import decman
|
||||
decman.execution_order = [
|
||||
"files",
|
||||
"pacman",
|
||||
"aur",
|
||||
"systemd",
|
||||
]
|
||||
```
|
||||
|
||||
This variable also manages which plugins are enabled. To enable flatpaks, simply add the plugin to the execution order.
|
||||
|
||||
```py
|
||||
import decman
|
||||
decman.execution_order = [
|
||||
"files",
|
||||
"pacman",
|
||||
"aur",
|
||||
"flatpak",
|
||||
"systemd",
|
||||
]
|
||||
```
|
||||
|
||||
Note that `files` is not a plugin, but is defined here anyways.
|
||||
|
||||
Before the core execution order, decman will run hook methods from `Module`s.
|
||||
|
||||
1. `before_update`
|
||||
1. `on_disable`
|
||||
|
||||
After the plugin execution, decman will run the following hook methods.
|
||||
|
||||
1. `on_enable`
|
||||
1. `on_change`
|
||||
1. `atfer_update`
|
||||
|
||||
Operations and hooks may be skipped with command line options.
|
||||
|
||||
## Why use decman?
|
||||
|
||||
@@ -341,14 +286,16 @@ You can consult your config to see what packages are installed and what config f
|
||||
In a modular config, you can also change parts of your system eg. switch shells without it affecting your other setups at all. If you create a module called `Shell` that exposes a function `add_alias`, you can call that function from other modules. Then later if you decide to switch from bash to fish, you can change the internals of your `Shell`-module without modifying your other modules at all.
|
||||
|
||||
```py
|
||||
from decman import Module
|
||||
|
||||
# Look below for an example of a theme module
|
||||
import theme
|
||||
|
||||
class Shell(Module):
|
||||
def __init__(self):
|
||||
super().__init__("shell", enabled=True, version="1")
|
||||
super().__init__("shell")
|
||||
self._aliases_text = ""
|
||||
|
||||
# --
|
||||
|
||||
def add_alias(self, alias: str, cmd: str):
|
||||
self._aliases_text += f"alias {alias}='{cmd}'\n"
|
||||
|
||||
@@ -358,7 +305,6 @@ class Shell(Module):
|
||||
File(source_file="./files/shell/config.fish", owner="user")
|
||||
}
|
||||
|
||||
|
||||
def file_variables(self) -> dict[str, str]:
|
||||
fvars = {
|
||||
"%aliases%": self._aliases_text,
|
||||
@@ -394,24 +340,34 @@ Using python you can use the same config for different computers and only change
|
||||
```py
|
||||
import socket
|
||||
|
||||
import decman
|
||||
|
||||
if socket.gethostname() == "laptop":
|
||||
# add brightness controls to your laptop
|
||||
decman.packages += ["brightnessctl"]
|
||||
decman.pacman.packages += ["brightnessctl"]
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
There are some alternatives you may want to consider instead of using decman.
|
||||
|
||||
- [Ansible](https://docs.ansible.com/)
|
||||
- [aconfmgr](https://github.com/CyberShadow/aconfmgr)
|
||||
- [NixOS](https://nixos.org/)
|
||||
|
||||
### Why not use NixOS?
|
||||
|
||||
NixOS is a Linux disto built around the idea of declarative system management, so why create a more limited alternative?
|
||||
|
||||
I tried NixOS in the past, but it had some issues that caused me to create decman for Arch Linux instead. In my personal opinion:
|
||||
I tried NixOS in the past, but it had some issues that caused me to create decman for Arch Linux instead. In my opinion:
|
||||
|
||||
- NixOS forces you to do everything the Nix way. Sometimes I just want to develop software without having to use nix tools.
|
||||
- NixOS is hard, and the documentation (when I last tried it) wasn't that good. Doing more complex stuff was sometimes just very annoying.
|
||||
- NixOS has unnecessary abstraction with NixOS options. They are great until you have to configure something specific and there is not an option for it. Then you'll have to inline other configuration language within your Nix config. And if some software doesn't have any premade options you'll have to do write the config manually. Then you'll have some software managed with just options and others with normal config files. I prefer to keep everything consistent.
|
||||
- NixOS forces you to do everything the Nix way.
|
||||
- NixOS requires learning a new domain specific language.
|
||||
- NixOS is extreme when it comes to declaration. Sometimes you don't want _everything_ to be managed declaratively.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2024 Kivi Kaitaniemi
|
||||
Copyright (C) 2024-2025 Kivi Kaitaniemi
|
||||
|
||||
Decman 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 3 of the License, or (at your option) any later version.
|
||||
|
||||
Reference in New Issue
Block a user