7 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
6 changed files with 174 additions and 36 deletions
+3 -3
View File
@@ -125,7 +125,7 @@ So far I have not created an AUR package for decman, because I'm not sure if oth
### 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.
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.
@@ -148,7 +148,7 @@ decman.user_packages.append(
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.2.1",
version="0.3.0",
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
make_dependencies=[
"python-setuptools", "python-build", "python-installer", "python-wheel"
@@ -276,7 +276,7 @@ user_packages = [{
pkgname="decman-git",
# Note, this example may not be up to date
provides=["decman"],
version="0.2.1",
version="0.3.0",
dependencies=["python", "python-requests", "devtools", "pacman", "systemd", "git"],
make_dependencies=[
"python-setuptools", "python-build", "python-installer", "python-wheel"
+1 -1
View File
@@ -82,7 +82,7 @@ class MyModule(Module):
return [
UserPackage(
pkgname="decman-git",
version="0.2.1",
version="0.3.0",
provides=["decman"],
dependencies=[
"python",
+27 -6
View File
@@ -40,7 +40,7 @@ decman.config.makepkg_user = "kk"
decman.user_packages.append(
UserPackage(
pkgname="decman-git",
version="0.2.1",
version="0.3.0",
provides=["decman"],
dependencies=[
"python",
@@ -132,6 +132,23 @@ decman.config.suppress_command_output = True
# Make output less verbose. Summaries are still printed.
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.
# 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]:
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]:
return ["pacman", "-S", "--needed"] + pkgs
return ["pacman", "-S", "--color=always", "--needed"] + pkgs
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]:
return ["pacman", "-D", "--asexplicit"] + pkgs
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]:
return ["pacman", "-Sddp", pkg]
def upgrade(self) -> list[str]:
return ["pacman", "-Syu"]
return ["pacman", "-Syu", "--color=always"]
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]:
return ["systemctl", "enable"] + units
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "decman"
version = "0.2.1"
version = "0.3.0"
description = "Declarative package & configuration manager for Arch Linux."
license = {file = "LICENSE"}
authors = [
+17 -6
View File
@@ -45,26 +45,27 @@ class Commands:
"""
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]:
"""
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]:
"""
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]:
"""
Running this command installs the given packages from pacman repositories.
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]:
"""
@@ -76,14 +77,14 @@ class Commands:
"""
Running this command upgrades all pacman packages.
"""
return ["pacman", "-Syu"]
return ["pacman", "-Syu", "--color=always"]
def remove(self, pkgs: list[str]) -> list[str]:
"""
Running this command removes the given packages and their dependencies
(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]:
"""
@@ -199,6 +200,16 @@ valid_pkgexts: list[str] = [
".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"
build_dir: str = "/tmp/decman/build"
pkg_cache_dir: str = "/var/cache/decman"
+125 -19
View File
@@ -2,7 +2,8 @@
Library module for decman.
"""
import pwd
import threading
import sys
import shutil
import subprocess
import json
@@ -755,14 +756,23 @@ class Pacman:
if not packages:
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:
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
"Failed to set packages as explicitly installed using pacman."
) from error
def install_dependencies(self, deps: list[str]):
"""
@@ -771,12 +781,14 @@ class Pacman:
if not deps:
return
try:
subprocess.run(conf.commands.install_deps(deps), check=True)
except subprocess.CalledProcessError as error:
returncode, output = echo_and_capture_command(
conf.commands.install_deps(deps))
if returncode != 0:
raise err.UserFacingError(
"Failed to install packages as dependencies using pacman."
) from error
f"Failed to install packages as dependencies using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def install_files(self, files: list[str], as_explicit: list[str]):
"""
@@ -786,9 +798,16 @@ class Pacman:
if not files:
return
try:
subprocess.run(conf.commands.install_files(files), check=True)
returncode, output = echo_and_capture_command(
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:
subprocess.run(
conf.commands.set_as_explicitly_installed(as_explicit),
@@ -799,17 +818,20 @@ class Pacman:
print_error("Output:")
print_continuation(error.output)
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):
"""
Upgrades all packages.
"""
try:
subprocess.run(conf.commands.upgrade(), check=True)
except subprocess.CalledProcessError as error:
returncode, output = echo_and_capture_command(conf.commands.upgrade())
if returncode != 0:
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]):
"""
@@ -817,11 +839,95 @@ class Pacman:
"""
if not packages:
return
try:
subprocess.run(conf.commands.remove(packages), check=True)
except subprocess.CalledProcessError as error:
returncode, output = echo_and_capture_command(
conf.commands.remove(packages))
if returncode != 0:
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: