13 Commits
Author SHA1 Message Date
Kivi Kaitaniemi 483bf0d676 Release v0.3.0 2024-07-06 18:52:14 +03:00
Kivi Kaitaniemi eb2ad7b1eb Merge pull request #4 from kiviktnm/pacman-color-always-as-default
Set pacman output use colors by default
2024-07-06 18:45:52 +03:00
Kivi Kaitaniemi cf1ae2ec50 Set pacman output use colors by default 2024-07-06 18:45:10 +03:00
Kivi Kaitaniemi 3b91577d61 Change pacman output highlight keyword defaults 2024-07-06 18:34:21 +03:00
Kivi Kaitaniemi 539bb3bfad Merge pull request #3 from kiviktnm/highlight-pacman-output
Add feature to print pacman output highlights
2024-07-06 18:23:13 +03:00
Kivi Kaitaniemi ab53a37f9f Add feature to print pacman output highlights 2024-07-06 18:20:10 +03:00
Kivi Kaitaniemi 1f8be0593f Update README.md 2024-05-30 15:06:43 +03:00
Kivi Kaitaniemi bf987c69d1 Release v0.2.1 2024-05-24 17:48:00 +03:00
Kivi Kaitaniemi 4d940ff0ac Merge pull request #1 from kiviktnm/improve-file-install
Better printing for file install
2024-05-24 17:44:42 +03:00
Kivi Kaitaniemi e93eed4309 Better printing for file install
- print all managed files including those defined with directories
- CLI option --print now also shows files that would be removed
- added --dry-run alias to --print
2024-05-24 17:41:33 +03:00
Kivi Kaitaniemi 61a06668eb Fix formatting 2024-05-23 17:27:58 +03:00
Kivi Kaitaniemi d568c6fa84 Update README.md with TOML example 2024-05-23 17:22:04 +03:00
Kivi Kaitaniemi 741e6611ce Add note about building packages to README.md 2024-05-21 19:04:36 +03:00
8 changed files with 395 additions and 144 deletions
+199 -87
View File
@@ -2,6 +2,8 @@
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. 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).
## Overview ## Overview
A complete example is available in the `example`-directory of this repository. It also serves as documentation so reading it is recommended. A complete example is available in the `example`-directory of this repository. It also serves as documentation so reading it is recommended.
@@ -102,6 +104,203 @@ Decman has some CLI options, to see them all run:
decman --help decman --help
``` ```
## Installation
Clone the decman PKGBUILD:
```sh
git clone https://github.com/kiviktnm/decman-pkgbuild.git
```
Review the PKGBUILD and install it.
```sh
cd decman-pkgbuild
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.
## What decman manages?
### Packages
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.
```py
# Include both foreign and pacman packages here.
decman.ignored_packages += ["yay", "opendoas"]
```
### Foreign packages
> [!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.
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.
```py
decman.user_packages.append(
decman.UserPackage(
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.3.0",
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",
))
```
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 tempfs for the `/tmp/` directory to speed up builds. Also make sure that the tempfs-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
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"
# 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))
```
Then you can use TOML configuration like this:
```toml
packages = ["python", "git", "networkmanager", "ufw", "neovim", "python-toml"]
aur_packages = ["protonvpn"]
enabled_systemd_units = ["NetworkManager.service"]
ignored_packages = ["yay"]
user_packages = [{
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.3.0",
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"
}]
[files]
'/etc/vconsole.conf' = { content="KEYMAP=us" }
'/etc/pacman.conf' = { source_file="./dotfiles/pacman.conf" }
[directories]
'/home/user/.config/nvim' = { source_directory="./dotfiles/nvim", owner="user" }
# 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"]
```
</details>
## Why use decman? ## Why use decman?
Here are some reasons why I created decman for myself. Here are some reasons why I created decman for myself.
@@ -183,93 +382,6 @@ I tried NixOS in the past, but it had some issues that caused me to create decma
- 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 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 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.
## Installation
Clone the decman PKGBUILD:
```sh
git clone https://github.com/kiviktnm/decman-pkgbuild.git
```
Review the PKGBUILD and install it.
```sh
cd decman-pkgbuild
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.
## What decman manages?
### Packages
Decman can be used to install pacman packages. Decman will install all packages defined in the source and **remove** all packages not defined in the source. You can set packages to be ignored by decman, so that it won't install them nor remove them.
```py
# Include both foreign and pacman packages here.
decman.ignored_packages += ["yay", "opendoas"]
```
### Foreign packages
> [!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.
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.
```py
decman.user_packages.append(
decman.UserPackage(
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.2.0",
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",
))
```
Building of foreign packages happens in a chroot. This creates some overhead, but ensures clean builds. Build packages are stored in a cache `/var/cache/decman`. By default decman keeps 3 most recent versions of all packages.
### Systemd units
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.
## License ## License
Copyright (C) 2024 Kivi Kaitaniemi Copyright (C) 2024 Kivi Kaitaniemi
+1 -1
View File
@@ -82,7 +82,7 @@ class MyModule(Module):
return [ return [
UserPackage( UserPackage(
pkgname="decman-git", pkgname="decman-git",
version="0.2.0", version="0.3.0",
provides=["decman"], provides=["decman"],
dependencies=[ dependencies=[
"python", "python",
+27 -6
View File
@@ -40,7 +40,7 @@ decman.config.makepkg_user = "kk"
decman.user_packages.append( decman.user_packages.append(
UserPackage( UserPackage(
pkgname="decman-git", pkgname="decman-git",
version="0.2.0", version="0.3.0",
provides=["decman"], provides=["decman"],
dependencies=[ dependencies=[
"python", "python",
@@ -132,6 +132,23 @@ decman.config.suppress_command_output = True
# Make output less verbose. Summaries are still printed. # Make output less verbose. Summaries are still printed.
decman.config.quiet_output = False decman.config.quiet_output = False
# Decman captures pacman command output, and any line (and adjacent lines) that contains any of
# the following keywords (case-insensetive) will be printed after the pacman command finishes.
#
# REMEMBER: You should still generally pay attention to pacman output
# since these keywords may not catch all cases.
decman.config.pacman_output_keywords = [
"pacsave",
"pacnew",
# Additional keywords can be:
#"warning",
#"error",
#"note",
# They might cause too many highlights however.
]
# If you don't want to print lines that contain keywords, set this to False
decman.config.print_pacman_output_highlights = True
# The user which builds aur and user packages. # The user which builds aur and user packages.
# decman.config.makepkg_user = "nobody" # This was set in a previous example. Let's not override it. # decman.config.makepkg_user = "nobody" # This was set in a previous example. Let's not override it.
@@ -163,26 +180,30 @@ class MyCommands(decman.config.Commands):
def list_foreign_pkgs_versioned(self) -> list[str]: def list_foreign_pkgs_versioned(self) -> list[str]:
return ["pacman", "-Qm", "--color=never"] return ["pacman", "-Qm", "--color=never"]
# --color=always is used in many commands since --color=auto results in no color.
# It seems a sensible default for me, since decman already uses color and it can't be disabled.
def install_pkgs(self, pkgs: list[str]) -> list[str]: def install_pkgs(self, pkgs: list[str]) -> list[str]:
return ["pacman", "-S", "--needed"] + pkgs return ["pacman", "-S", "--color=always", "--needed"] + pkgs
def install_files(self, pkg_files: list[str]) -> list[str]: def install_files(self, pkg_files: list[str]) -> list[str]:
return ["pacman", "-U", "--asdeps"] + pkg_files return ["pacman", "-U", "--color=always", "--asdeps"] + pkg_files
def set_as_explicitly_installed(self, pkgs: list[str]) -> list[str]: def set_as_explicitly_installed(self, pkgs: list[str]) -> list[str]:
return ["pacman", "-D", "--asexplicit"] + pkgs return ["pacman", "-D", "--asexplicit"] + pkgs
def install_deps(self, deps: list[str]) -> list[str]: def install_deps(self, deps: list[str]) -> list[str]:
return ["pacman", "-S", "--needed", "--asdeps"] + deps return ["pacman", "-S", "--color=always", "--needed", "--asdeps"
] + deps
def is_installable(self, pkg: str) -> list[str]: def is_installable(self, pkg: str) -> list[str]:
return ["pacman", "-Sddp", pkg] return ["pacman", "-Sddp", pkg]
def upgrade(self) -> list[str]: def upgrade(self) -> list[str]:
return ["pacman", "-Syu"] return ["pacman", "-Syu", "--color=always"]
def remove(self, pkgs: list[str]) -> list[str]: def remove(self, pkgs: list[str]) -> list[str]:
return ["pacman", "-Rs"] + pkgs return ["pacman", "-Rs", "--color=always"] + pkgs
def enable_units(self, units: list[str]) -> list[str]: def enable_units(self, units: list[str]) -> list[str]:
return ["systemctl", "enable"] + units return ["systemctl", "enable"] + units
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "decman" name = "decman"
version = "0.2.0" version = "0.3.0"
description = "Declarative package & configuration manager for Arch Linux." description = "Declarative package & configuration manager for Arch Linux."
license = {file = "LICENSE"} license = {file = "LICENSE"}
authors = [ authors = [
+5 -3
View File
@@ -217,10 +217,10 @@ class Directory:
if group is not None: if group is not None:
self.gid = grp.getgrnam(group).gr_gid self.gid = grp.getgrnam(group).gr_gid
def copy_to( def copy_to(self,
self,
target_directory: str, target_directory: str,
variables: typing.Optional[dict[str, str]] = None) -> list[str]: variables: typing.Optional[dict[str, str]] = None,
only_print: bool = False) -> list[str]:
""" """
Copies the files in this directory to the target directory. Copies the files in this directory to the target directory.
@@ -242,6 +242,8 @@ class Directory:
target = os.path.normpath( target = os.path.normpath(
os.path.join(target_directory, src_path)) os.path.join(target_directory, src_path))
created.append(target) created.append(target)
if not only_print:
file.copy_to(target, variables) file.copy_to(target, variables)
finally: finally:
os.chdir(original_wd) os.chdir(original_wd)
+10 -16
View File
@@ -32,11 +32,10 @@ def main():
help="python file containing configuration") help="python file containing configuration")
parser.add_argument( parser.add_argument(
"--print", "--print",
"--dry-run",
action="store_true", action="store_true",
default=False, default=False,
help= help="print what would happen as a result of running decman")
"print what would happen as a result of running decman (doesn't print removed files)"
)
parser.add_argument("--debug", parser.add_argument("--debug",
action="store_true", action="store_true",
default=False, default=False,
@@ -263,23 +262,18 @@ class Core:
def _create_and_remove_files(self): def _create_and_remove_files(self):
l.print_summary("Installing files.") l.print_summary("Installing files.")
l.print_list("Files to install:",
self.source.all_file_targets(), all_created = self.source.create_all_files(self.only_print)
elements_per_line=1, to_remove = self.source.files_to_remove(self.store, all_created)
level=l.INFO)
l.print_list("Directories to install:", l.print_list("Ensured files are up to date:",
self.source.all_directory_targets(), all_created,
elements_per_line=1, elements_per_line=1)
level=l.INFO) l.print_list("Removing files:", to_remove, elements_per_line=1)
if self.only_print: if self.only_print:
return return
all_created = self.source.create_all_files()
to_remove = self.source.files_to_remove(self.store, all_created)
l.print_list("Removing files:", to_remove, elements_per_line=1)
for file in to_remove: for file in to_remove:
try: try:
os.remove(file) os.remove(file)
+17 -6
View File
@@ -45,26 +45,27 @@ class Commands:
""" """
Running this command installs the given packages from pacman repositories. Running this command installs the given packages from pacman repositories.
""" """
return ["pacman", "-S", "--needed"] + pkgs return ["pacman", "-S", "--color=always", "--needed"] + pkgs
def install_files(self, pkg_files: list[str]) -> list[str]: def install_files(self, pkg_files: list[str]) -> list[str]:
""" """
Running this command installs the given packages files. Running this command installs the given packages files.
""" """
return ["pacman", "-U", "--asdeps"] + pkg_files return ["pacman", "-U", "--color=always", "--asdeps"] + pkg_files
def set_as_explicitly_installed(self, pkgs: list[str]) -> list[str]: def set_as_explicitly_installed(self, pkgs: list[str]) -> list[str]:
""" """
Running this command installs sets the given as explicitly installed. Running this command installs sets the given as explicitly installed.
""" """
return ["pacman", "-D", "--asexplicit"] + pkgs return ["pacman", "-D", "--color=always", "--asexplicit"] + pkgs
def install_deps(self, deps: list[str]) -> list[str]: def install_deps(self, deps: list[str]) -> list[str]:
""" """
Running this command installs the given packages from pacman repositories. Running this command installs the given packages from pacman repositories.
The packages are installed as dependencies. The packages are installed as dependencies.
""" """
return ["pacman", "-S", "--needed", "--asdeps"] + deps return ["pacman", "-S", "--color=always", "--needed", "--asdeps"
] + deps
def is_installable(self, pkg: str) -> list[str]: def is_installable(self, pkg: str) -> list[str]:
""" """
@@ -76,14 +77,14 @@ class Commands:
""" """
Running this command upgrades all pacman packages. Running this command upgrades all pacman packages.
""" """
return ["pacman", "-Syu"] return ["pacman", "-Syu", "--color=always"]
def remove(self, pkgs: list[str]) -> list[str]: def remove(self, pkgs: list[str]) -> list[str]:
""" """
Running this command removes the given packages and their dependencies Running this command removes the given packages and their dependencies
(that aren't required by other packages). (that aren't required by other packages).
""" """
return ["pacman", "-Rs"] + pkgs return ["pacman", "-Rs", "--color=always"] + pkgs
def enable_units(self, units: list[str]) -> list[str]: def enable_units(self, units: list[str]) -> list[str]:
""" """
@@ -199,6 +200,16 @@ valid_pkgexts: list[str] = [
".pkg.tar.Z", ".pkg.tar.Z",
] ]
pacman_output_keywords: list[str] = [
"pacsave",
"pacnew",
# These cause too many false positives IMO
#"warning",
#"error",
#"note",
]
print_pacman_output_highlights: bool = True
makepkg_user: str = "nobody" makepkg_user: str = "nobody"
build_dir: str = "/tmp/decman/build" build_dir: str = "/tmp/decman/build"
pkg_cache_dir: str = "/var/cache/decman" pkg_cache_dir: str = "/var/cache/decman"
+133 -22
View File
@@ -2,7 +2,8 @@
Library module for decman. Library module for decman.
""" """
import pwd import threading
import sys
import shutil import shutil
import subprocess import subprocess
import json import json
@@ -459,7 +460,7 @@ class Source:
elif module.enabled and module.name not in store.enabled_modules: elif module.enabled and module.name not in store.enabled_modules:
module.after_version_change() module.after_version_change()
def create_all_files(self) -> list[str]: def create_all_files(self, only_print: bool) -> list[str]:
""" """
Creates all files and returns them. The files created are based on the specified files, Creates all files and returns them. The files created are based on the specified files,
directories and modules. directories and modules.
@@ -470,9 +471,13 @@ class Source:
variables: typing.Optional[dict[str, str]] = None): variables: typing.Optional[dict[str, str]] = None):
for target, file in files.items(): for target, file in files.items():
created_files.append(target) created_files.append(target)
if only_print:
continue
try: try:
file.copy_to(target, variables)
print_debug(f"Installing file to {target}.") print_debug(f"Installing file to {target}.")
file.copy_to(target, variables)
except OSError as e: except OSError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError(
@@ -483,7 +488,8 @@ class Source:
for target, directory in dirs.items(): for target, directory in dirs.items():
try: try:
print_debug(f"Installing directory to {target}.") print_debug(f"Installing directory to {target}.")
created_files.extend(directory.copy_to(target, variables)) created_files.extend(
directory.copy_to(target, variables, only_print))
except OSError as e: except OSError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError(
@@ -750,14 +756,23 @@ class Pacman:
if not packages: if not packages:
return return
returncode, output = echo_and_capture_command(
conf.commands.install_pkgs(packages))
if returncode != 0:
raise err.UserFacingError(
f"Failed to install packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
try: try:
subprocess.run(conf.commands.install_pkgs(packages), check=True)
subprocess.run(conf.commands.set_as_explicitly_installed(packages), subprocess.run(conf.commands.set_as_explicitly_installed(packages),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install packages using pacman.") from error "Failed to set packages as explicitly installed using pacman."
) from error
def install_dependencies(self, deps: list[str]): def install_dependencies(self, deps: list[str]):
""" """
@@ -766,12 +781,14 @@ class Pacman:
if not deps: if not deps:
return return
try: returncode, output = echo_and_capture_command(
subprocess.run(conf.commands.install_deps(deps), check=True) conf.commands.install_deps(deps))
except subprocess.CalledProcessError as error: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install packages as dependencies using pacman." f"Failed to install packages as dependencies using pacman. Process exited with code {returncode}."
) from error )
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def install_files(self, files: list[str], as_explicit: list[str]): def install_files(self, files: list[str], as_explicit: list[str]):
""" """
@@ -781,9 +798,16 @@ class Pacman:
if not files: if not files:
return return
try: returncode, output = echo_and_capture_command(
subprocess.run(conf.commands.install_files(files), check=True) conf.commands.install_files(files))
if returncode != 0:
raise err.UserFacingError(
f"Failed to install package files using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
try:
if as_explicit: if as_explicit:
subprocess.run( subprocess.run(
conf.commands.set_as_explicitly_installed(as_explicit), conf.commands.set_as_explicitly_installed(as_explicit),
@@ -794,17 +818,20 @@ class Pacman:
print_error("Output:") print_error("Output:")
print_continuation(error.output) print_continuation(error.output)
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install package files using pacman.") from error "Failed to set packages as explicitly installed using pacman."
) from error
def upgrade(self): def upgrade(self):
""" """
Upgrades all packages. Upgrades all packages.
""" """
try: returncode, output = echo_and_capture_command(conf.commands.upgrade())
subprocess.run(conf.commands.upgrade(), check=True) if returncode != 0:
except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to upgrade packages using pacman.") from error f"Failed to upgrade packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def remove(self, packages: list[str]): def remove(self, packages: list[str]):
""" """
@@ -812,11 +839,95 @@ class Pacman:
""" """
if not packages: if not packages:
return return
try:
subprocess.run(conf.commands.remove(packages), check=True) returncode, output = echo_and_capture_command(
except subprocess.CalledProcessError as error: conf.commands.remove(packages))
if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to remove packages using pacman.") from error f"Failed to remove packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def print_highlighted_pacman_messages(output: str):
"""
Prints lines that contain pacman output keywords.
"""
print_summary("Pacman output highlights:")
lines = output.split("\n")
for index, line in enumerate(lines):
for keyword in conf.pacman_output_keywords:
if keyword.lower() in line.lower():
print_summary(f"lines: {index}-{index+2}")
if index >= 1:
print_continuation(lines[index - 1])
print_continuation(line)
if index + 1 < len(lines):
print_continuation(lines[index + 1])
print_continuation("")
# Break, as to not print the same line again if it contains multiple keywords
break
def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
"""
Runs the given CLI program and arguments.
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()
os.set_blocking(sys.stdin.fileno(), False)
# Capture stdin and forward it to the process in a non-blocking manner
while process.poll() is None:
inp = sys.stdin.readline()
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)
class _OutputCapturingThread(threading.Thread):
def __init__(self, stream):
super().__init__()
self._stream = stream
self.output = ""
self.done = False
def run(self):
while not self.done and not self._stream.closed:
output = self._stream.read()
if output:
output = output.decode()
self.output += output
print(output, end="")
time.sleep(0.1)
class Systemd: class Systemd: