12 Commits
Author SHA1 Message Date
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
Kivi Kaitaniemi d36fec6e03 Release v0.2.0 2024-05-19 04:09:20 +03:00
Kivi Kaitaniemi 672afbcddc Update README.md and the example 2024-05-19 04:06:09 +03:00
Kivi Kaitaniemi 97b6069080 Fix bug with removing fpkg from the cache 2024-05-19 03:41:51 +03:00
Kivi Kaitaniemi 06a0f573cd Improve printed output 2024-05-19 03:05:46 +03:00
Kivi Kaitaniemi 0d8118dd0e Dont start enabled systemd units immediately 2024-05-18 02:31:40 +03:00
Kivi Kaitaniemi 09c320cdad Fix bug with installing pacman packags & systemd user units 2024-05-18 01:05:11 +03:00
9 changed files with 339 additions and 201 deletions
+200 -88
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.
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
A complete example is available in the `example`-directory of this repository. It also serves as documentation so reading it is recommended.
@@ -81,7 +83,7 @@ from syncthing import Syncthing
decman.modules += [Syncthing()]
```
Then run decman.
Then run decman. Note that terminal colors cannot be disabled for decman.
> [!WARNING]
> Decman runs as root. This means that your `source.py` will be executed as root as well.
@@ -102,6 +104,203 @@ Decman has some CLI options, to see them all run:
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 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.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",
))
```
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.2.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"
}]
[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?
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 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.1.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
Copyright (C) 2024 Kivi Kaitaniemi
+1 -1
View File
@@ -82,7 +82,7 @@ class MyModule(Module):
return [
UserPackage(
pkgname="decman-git",
version="0.1.0",
version="0.2.1",
provides=["decman"],
dependencies=[
"python",
+13 -13
View File
@@ -4,7 +4,7 @@
import socket
import os
# Remember: Do NOT use from imports for global variables
# Note: Do NOT use from imports for global variables
# BAD: from decman import packages/modules/etc
import decman
import decman.config
@@ -40,7 +40,7 @@ decman.config.makepkg_user = "kk"
decman.user_packages.append(
UserPackage(
pkgname="decman-git",
version="0.1.0",
version="0.2.1",
provides=["decman"],
dependencies=[
"python",
@@ -123,15 +123,15 @@ decman.modules += [my_own_mod]
# Configuring the behavior of decman is also done here.
# These are the default values.
# Note: you probably don't want to change these 2 settings and instead you'll want to to use the --debug CLI option.
# Show debug output
decman.config.debug_output = False
# Suppress output of some commands that you probably don't want to see.
decman.config.suppress_command_output = True
# Make output less verbose. Summaries are still printed.
decman.config.quiet_output = False
# Suppress output of some commands that you probably don't want to see.
decman.config.suppress_command_output = True
# 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.
@@ -164,7 +164,7 @@ class MyCommands(decman.config.Commands):
return ["pacman", "-Qm", "--color=never"]
def install_pkgs(self, pkgs: list[str]) -> list[str]:
return ["pacman", "-S", "--asexplicit"] + pkgs
return ["pacman", "-S", "--needed"] + pkgs
def install_files(self, pkg_files: list[str]) -> list[str]:
return ["pacman", "-U", "--asdeps"] + pkg_files
@@ -185,16 +185,16 @@ class MyCommands(decman.config.Commands):
return ["pacman", "-Rs"] + pkgs
def enable_units(self, units: list[str]) -> list[str]:
return ["systemctl", "enable", "--now", "--quiet"] + units
return ["systemctl", "enable"] + units
def disable_units(self, units: list[str]) -> list[str]:
return ["systemctl", "disable", "--quiet"] + units
return ["systemctl", "disable"] + units
def enable_user_units(self, units: list[str]) -> list[str]:
return ["systemctl", "enable", "--now", "--quiet", "--user"] + units
def enable_user_units(self, units: list[str], user: str) -> list[str]:
return ["systemctl", "--user", "-M", f"{user}@", "enable"] + units
def disable_user_units(self, units: list[str]) -> list[str]:
return ["systemctl", "disable", "--quiet"] + units
def disable_user_units(self, units: list[str], user: str) -> list[str]:
return ["systemctl", "--user", "-M", f"{user}@", "disable"] + units
def compare_versions(self, installed_version: str,
new_version: str) -> list[str]:
@@ -251,7 +251,7 @@ class PikaurWrapperCommands(decman.config.Commands):
return ["pikaur", "-Qeq"]
def install_pkgs(self, pkgs: list[str]) -> list[str]:
return ["pikaur", "-S", "--asexplicit"] + pkgs
return ["pikaur", "-S"] + pkgs
def upgrade(self) -> list[str]:
return ["pikaur", "-Syu"]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "decman"
version = "0.1.0"
version = "0.2.1"
description = "Declarative package & configuration manager for Arch Linux."
license = {file = "LICENSE"}
authors = [
+7 -5
View File
@@ -217,10 +217,10 @@ class Directory:
if group is not None:
self.gid = grp.getgrnam(group).gr_gid
def copy_to(
self,
target_directory: str,
variables: typing.Optional[dict[str, str]] = None) -> list[str]:
def copy_to(self,
target_directory: str,
variables: typing.Optional[dict[str, str]] = None,
only_print: bool = False) -> list[str]:
"""
Copies the files in this directory to the target directory.
@@ -242,7 +242,9 @@ class Directory:
target = os.path.normpath(
os.path.join(target_directory, src_path))
created.append(target)
file.copy_to(target, variables)
if not only_print:
file.copy_to(target, variables)
finally:
os.chdir(original_wd)
return created
+29 -22
View File
@@ -32,11 +32,14 @@ def main():
help="python file containing configuration")
parser.add_argument(
"--print",
"--dry-run",
action="store_true",
default=False,
help=
"print what would happen as a result of running decman (doesn't print removed files)"
)
help="print what would happen as a result of running decman")
parser.add_argument("--debug",
action="store_true",
default=False,
help="show debug output")
parser.add_argument(
"--no-packages",
action="store_true",
@@ -88,6 +91,13 @@ def main():
try:
opts = _set_up(store, args)
# Override debug_output if cli option is used
if args.debug:
conf.debug_output = True
conf.suppress_command_output = False
# When print cli option is used, show info output
if args.print:
conf.quiet_output = False
Core(store, opts).run()
except err.UserFacingError as error:
l.print_error(error.user_facing_msg)
@@ -207,20 +217,20 @@ class Core:
def _disable_units(self):
to_disable = self.source.units_to_disable(self.store)
l.print_list_summary("Disabling systemd units:", to_disable)
l.print_list("Disabling systemd units:", to_disable)
if not self.only_print:
self.systemctl.disable_units(to_disable)
user_units_to_disable = self.source.user_units_to_disable(self.store)
for user, units in user_units_to_disable.items():
l.print_list_summary(f"Disabling systemd units for {user}:", units)
l.print_list(f"Disabling systemd units for {user}:", units)
if not self.only_print:
self.systemctl.disable_user_units(units, user)
def _remove_pkgs(self):
currently_installed = self.pacman.get_installed()
to_remove = self.source.packages_to_remove(currently_installed)
l.print_list_summary("Removing packages:", to_remove)
l.print_list("Removing packages:", to_remove)
if not self.only_print:
self.pacman.remove(to_remove)
@@ -239,12 +249,11 @@ class Core:
to_install_fpm = self.source.foreign_packages_to_install(
currently_installed)
l.print_list_summary("Installing pacman packages:", to_install_pacman)
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_summary("Installing foreign packages:",
to_install_fpm)
l.print_list("Installing foreign packages:", to_install_fpm)
if not self.only_print:
self.pacman.install(to_install_pacman)
@@ -252,21 +261,19 @@ class Core:
self.fpm.install(to_install_fpm, force=self.force_build)
def _create_and_remove_files(self):
l.print_list_summary("Installing files:",
self.source.all_file_targets(),
elements_per_line=1)
l.print_list_summary("Installing directories:",
self.source.all_directory_targets(),
elements_per_line=1)
l.print_summary("Installing files.")
all_created = self.source.create_all_files(self.only_print)
to_remove = self.source.files_to_remove(self.store, all_created)
l.print_list("Ensured files are up to date:",
all_created,
elements_per_line=1)
l.print_list("Removing files:", to_remove, elements_per_line=1)
if self.only_print:
return
all_created = self.source.create_all_files()
to_remove = self.source.files_to_remove(self.store, all_created)
l.print_list_summary("Removing files:", to_remove, elements_per_line=1)
for file in to_remove:
try:
os.remove(file)
@@ -278,13 +285,13 @@ class Core:
def _enable_units(self):
to_enable = self.source.units_to_enable(self.store)
l.print_list_summary("Enabling systemd units:", to_enable)
l.print_list("Enabling systemd units:", to_enable)
if not self.only_print:
self.systemctl.enable_units(to_enable)
user_units_to_enable = self.source.user_units_to_enable(self.store)
for user, units in user_units_to_enable.items():
l.print_list_summary(f"Enabling systemd units for {user}:", units)
l.print_list(f"Enabling systemd units for {user}:", units)
if not self.only_print:
self.systemctl.enable_user_units(units, user)
+9 -9
View File
@@ -45,7 +45,7 @@ class Commands:
"""
Running this command installs the given packages from pacman repositories.
"""
return ["pacman", "-S", "--asexplicit"] + pkgs
return ["pacman", "-S", "--needed"] + pkgs
def install_files(self, pkg_files: list[str]) -> list[str]:
"""
@@ -89,25 +89,25 @@ class Commands:
"""
Running this command enables the given systemd units.
"""
return ["systemctl", "enable", "--now", "--quiet"] + units
return ["systemctl", "enable"] + units
def disable_units(self, units: list[str]) -> list[str]:
"""
Running this command disables the given systemd units.
"""
return ["systemctl", "disable", "--quiet"] + units
return ["systemctl", "disable"] + units
def enable_user_units(self, units: list[str]) -> list[str]:
def enable_user_units(self, units: list[str], user: str) -> list[str]:
"""
Running this command enables the given systemd units for the user it's run as.
Running this command enables the given systemd units for the user.
"""
return ["systemctl", "enable", "--now", "--quiet", "--user"] + units
return ["systemctl", "--user", "-M", f"{user}@", "enable"] + units
def disable_user_units(self, units: list[str]) -> list[str]:
def disable_user_units(self, units: list[str], user: str) -> list[str]:
"""
Running this command disables the given systemd units fol the user it's run as.
Running this command disables the given systemd units for the user.
"""
return ["systemctl", "disable", "--quiet"] + units
return ["systemctl", "--user", "-M", f"{user}@", "disable"] + units
def compare_versions(self, installed_version: str,
new_version: str) -> list[str]:
+60 -46
View File
@@ -23,12 +23,16 @@ _RESET_SUFFIX = "\033[m"
_SPACING = " "
_CONTINUATION_PREFIX = f"{_DECMAN_MSG_TAG}{_SPACING} "
INFO = 1
SUMMARY = 2
def print_continuation(msg: str):
def print_continuation(msg: str, level: int = SUMMARY):
"""
Prints a message without a prefix.
"""
print(f"{_CONTINUATION_PREFIX}{msg}")
if level == SUMMARY or conf.debug_output or not conf.quiet_output:
print(f"{_CONTINUATION_PREFIX}{msg}")
def print_error(error_msg: str):
@@ -55,11 +59,12 @@ def print_summary(msg: str):
print(f"{_DECMAN_MSG_TAG} {_CYAN_PREFIX}SUMMARY{_RESET_SUFFIX}: {msg}")
def print_list_summary(msg: str,
l: list[str],
elements_per_line: typing.Optional[int] = None,
max_line_width: typing.Optional[int] = None,
limit_to_term_size: bool = True):
def print_list(msg: str,
l: list[str],
elements_per_line: typing.Optional[int] = None,
max_line_width: typing.Optional[int] = None,
limit_to_term_size: bool = True,
level: int = SUMMARY):
"""
Prints a summary message to the user along with a list of elements.
@@ -69,8 +74,12 @@ def print_list_summary(msg: str,
return
l = l.copy()
print_summary(msg)
print_continuation("")
if level == SUMMARY:
print_summary(msg)
elif level == INFO:
print_info(msg)
print_continuation("", level=level)
if elements_per_line is None:
elements_per_line = len(l)
@@ -100,9 +109,9 @@ def print_list_summary(msg: str,
elements_in_current_line = 1
for line in lines:
print_continuation(line)
print_continuation(line, level=level)
print_continuation("")
print_continuation("", level=level)
def print_info(msg: str):
@@ -263,14 +272,20 @@ class Store:
"""
new_entry = (version, path_to_built_pkg, int(time.time()))
entries = self._package_file_cache.get(package, [])
for _, already_cached_path, __ in entries:
if already_cached_path == path_to_built_pkg:
print_debug(
f"Trying to cache {package} version {version}, but the version is already cached: {already_cached_path}"
)
return
entries.append(new_entry)
self._package_file_cache[package] = entries
self._clean_pkg_cache(package)
def _clean_pkg_cache(self, package: str):
oldest_version = None
oldest_path = None
oldest_timestamp = None
index_of_oldest = None
entries = self._package_file_cache[package]
print_debug(f"Package cache has {len(entries)} entries.")
@@ -279,20 +294,19 @@ class Store:
print_debug("Old files will not be removed.")
return
for entry in entries:
version, path, timestamp = entry
for index, entry in enumerate(entries):
_, path, timestamp = entry
if oldest_timestamp is None or oldest_timestamp > timestamp:
oldest_version = version
oldest_timestamp = timestamp
oldest_path = path
index_of_oldest = index
print_debug(f"Oldest cached file for {package} is '{oldest_path}'.")
if oldest_path is None:
return
assert oldest_version is not None
assert oldest_timestamp is not None
assert index_of_oldest is not None
entries.remove((oldest_version, oldest_path, oldest_timestamp))
entries.pop(index_of_oldest)
if os.path.exists(oldest_path):
print_debug(f"Removing '{oldest_path}' from the package cache.")
try:
@@ -445,7 +459,7 @@ class Source:
elif module.enabled and module.name not in store.enabled_modules:
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,
directories and modules.
@@ -456,9 +470,13 @@ class Source:
variables: typing.Optional[dict[str, str]] = None):
for target, file in files.items():
created_files.append(target)
if only_print:
continue
try:
file.copy_to(target, variables)
print_debug(f"Installing file to {target}.")
file.copy_to(target, variables)
except OSError as e:
print_error(f"{e}")
raise err.UserFacingError(
@@ -469,7 +487,8 @@ class Source:
for target, directory in dirs.items():
try:
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:
print_error(f"{e}")
raise err.UserFacingError(
@@ -738,6 +757,9 @@ class Pacman:
try:
subprocess.run(conf.commands.install_pkgs(packages), check=True)
subprocess.run(conf.commands.set_as_explicitly_installed(packages),
check=True,
capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
"Failed to install packages using pacman.") from error
@@ -818,7 +840,9 @@ class Systemd:
return
try:
subprocess.run(conf.commands.enable_units(units), check=True)
subprocess.run(conf.commands.enable_units(units),
check=True,
capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
f"Failed to enable systemd units: {units}") from error
@@ -832,7 +856,9 @@ class Systemd:
return
try:
subprocess.run(conf.commands.disable_units(units), check=True)
subprocess.run(conf.commands.disable_units(units),
check=True,
capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
f"Failed to disable systemd units: {units}") from error
@@ -850,19 +876,14 @@ class Systemd:
return
try:
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(conf.commands.enable_user_units(units),
group=gid,
user=uid) as process:
if process.wait() != 0:
raise err.UserFacingError(
f"Failed to enable systemd units: {units} for {user}.")
except KeyError as error:
subprocess.run(conf.commands.enable_user_units(units, user),
check=True,
capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
f"Failed to enable systemd units because user {user} doesn't exist."
f"Failed to enable systemd units: {units} for {user}."
) from error
for unit in units:
self.state.add_enabled_user_systemd_unit(user, unit)
@@ -874,19 +895,12 @@ class Systemd:
return
try:
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(conf.commands.disable_user_units(units),
group=gid,
user=uid) as process:
if process.wait() != 0:
raise err.UserFacingError(
f"Failed to disable systemd units: {units} for {user}."
)
except KeyError as error:
subprocess.run(conf.commands.disable_user_units(units, user),
check=True,
capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error:
raise err.UserFacingError(
f"Failed to disable systemd units because user {user} doesn't exist."
f"Failed to disable systemd units: {units} for {user}."
) from error
for unit in units:
+19 -16
View File
@@ -472,7 +472,7 @@ class ExtendedPackageSearch:
providers = "Providers: "
for index, name in enumerate(possible_providers):
providers += f"{index + 1}:{name} "
l.print_info(providers)
l.print_summary(providers)
selection = l.prompt_number(
f"Select a provider [{min_selection}-{max_selection}] (default: {min_selection}): ",
@@ -557,7 +557,7 @@ class ForeignPackageManager:
if ignored_pkgs is None:
ignored_pkgs = set()
l.print_summary("Determining packages to upgrade.")
l.print_summary("Determining foreign packages to upgrade.")
all_foreign_pkgs = self._pacman.get_versioned_foreign_packages()
all_explicit_pkgs = set(self._pacman.get_installed())
@@ -609,17 +609,20 @@ class ForeignPackageManager:
resolved_dependencies = self.resolve_dependencies(
foreign_pkgs, foreign_dep_pkgs)
l.print_list_summary(
l.print_list(
"The following foreign packages will be installed explicitly:",
list(resolved_dependencies.foreign_pkgs))
list(resolved_dependencies.foreign_pkgs),
level=l.SUMMARY)
l.print_list_summary(
l.print_list(
"The following foreign packages will be installed as dependencies:",
list(resolved_dependencies.foreign_dep_pkgs))
list(resolved_dependencies.foreign_dep_pkgs),
level=l.SUMMARY)
l.print_list_summary(
l.print_list(
"The following foreign packages will be built in order to install other packages. They will not be installed:",
list(resolved_dependencies.foreign_build_dep_pkgs))
list(resolved_dependencies.foreign_build_dep_pkgs),
level=l.SUMMARY)
if not l.prompt_confirm("Proceed?", default=True):
raise err.UserFacingError("Installing aborted.")
@@ -675,7 +678,7 @@ class ForeignPackageManager:
Resolves foreign dependencies of foreign packages.
"""
l.print_summary("Resolving foreign package dependencies.")
l.print_info("Resolving foreign package dependencies.")
l.print_debug(f"Packages: {foreign_pkgs}")
if foreign_dep_pkgs is None:
@@ -739,7 +742,7 @@ class ForeignPackageManager:
total_processed += 1
l.print_info(f"Progress: {total_processed}/{len(seen_packages)}.")
l.print_summary("Determining build order.")
l.print_info("Determining build order.")
while True:
to_add = graph.get_and_remove_outer_dep_pkgs()
@@ -831,7 +834,7 @@ class PackageBuilder:
"""
Creates a new chroot and clones all PKGBUILDS.
"""
l.print_summary("Creating a build environment..")
l.print_info("Creating a build environment..")
if os.path.exists(conf.build_dir):
l.print_info("Removing previous build directory.")
@@ -858,7 +861,7 @@ class PackageBuilder:
self._git_clone_and_review_pkgbuild(pkgbase, git_url)
shutil.chown(pkgbuild_dir, user=conf.makepkg_user)
l.print_summary("Creating a new chroot.")
l.print_info("Creating a new chroot.")
os.makedirs(self.chroot_wd_dir)
# Remove GNUPGHOME from mkarchroot environment variables since it may interfere with
@@ -896,12 +899,12 @@ class PackageBuilder:
# Rebuild is only needed if at least one package is not in the cache.
if self._are_all_pkgs_cached(packages) and not force:
l.print_summary(
l.print_info(
f"Skipped building '{' '.join(package_names)}'. Already up to date."
)
return
l.print_summary(f"Building '{' '.join(package_names)}'.")
l.print_info(f"Building '{' '.join(package_names)}'.")
chroot_new_pacman_pkgs, chroot_pkg_files = self._get_chroot_packages(
packages)
@@ -960,7 +963,7 @@ class PackageBuilder:
check=True,
capture_output=conf.suppress_command_output)
l.print_summary(f"Finished building: '{' '.join(package_names)}'.")
l.print_info(f"Finished building: '{' '.join(package_names)}'.")
def _are_all_pkgs_cached(self, pkgs: list[ForeignPackage]) -> bool:
for pkg in pkgs:
@@ -1071,7 +1074,7 @@ before and thus are found in the cache."
check=True,
capture_output=conf.suppress_command_output)
if l.prompt_confirm(f"Review PKGBUILD for {pkgbase}?",
if l.prompt_confirm(f"Review PKGBUILD or show diff for {pkgbase}?",
default=True):
latest_reviewed_commit = self._store.pkgbuild_latest_reviewed_commits.get(
pkgbase)