Add sh/cmd functions to simplify running commands

This commit is contained in:
Kivi Kaitaniemi
2024-04-26 00:02:36 +03:00
parent 468d44ae01
commit c97587b3f5
5 changed files with 118 additions and 47 deletions
+66
View File
@@ -7,6 +7,72 @@ import pwd
import grp import grp
import shutil import shutil
import os import os
import subprocess
import decman.error
def sh(sh_cmd: str,
user: typing.Optional[str] = None,
env_overrides: typing.Optional[dict[str, str]] = None):
"""
Shortcut for running a shell command.
"""
if env_overrides is None:
env_overrides = {}
env = os.environ.copy()
for var, val in env_overrides.items():
env[var] = val
try:
if user is None:
subprocess.run(sh_cmd, shell=True, check=True, env=env)
else:
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(sh_cmd,
shell=True,
group=gid,
user=uid,
env=env) as process:
if process.wait() != 0:
raise decman.error.UserFacingError(
f"Running user shell command '{sh_cmd}' as {user} failed."
)
except (subprocess.CalledProcessError, KeyError) as e:
raise decman.error.UserFacingError(
f"Running user shell command '{sh_cmd}' failed.") from e
def cmd(command: list[str],
user: typing.Optional[str] = None,
env_overrides: typing.Optional[dict[str, str]] = None):
"""
Shortcut for running a command.
"""
if env_overrides is None:
env_overrides = {}
env = os.environ.copy()
for var, val in env_overrides.items():
env[var] = val
try:
if user is None:
subprocess.run(command, check=True, env=env)
else:
uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(command, group=gid, user=uid,
env=env) as process:
if process.wait() != 0:
raise decman.error.UserFacingError(
f"Running user command '{command}' as {user} failed.")
except (subprocess.CalledProcessError, KeyError) as e:
raise decman.error.UserFacingError(
f"Running user command '{command}' failed.") from e
class File: class File:
+12
View File
@@ -0,0 +1,12 @@
"""
Errors used by decman.
"""
class UserFacingError(Exception):
"""
Execution of an important step failed and the program shouldn't continue.
"""
def __init__(self, user_facing_msg: str):
self.user_facing_msg = user_facing_msg
+21 -30
View File
@@ -8,6 +8,7 @@ import json
import os import os
import typing import typing
import decman.config as conf import decman.config as conf
import decman.error as err
import decman import decman
_DECMAN_MSG_TAG = "[\033[1;35mDECMAN\033[m]" _DECMAN_MSG_TAG = "[\033[1;35mDECMAN\033[m]"
@@ -181,7 +182,7 @@ class Store:
with open(path, "wt", encoding="utf-8") as file: with open(path, "wt", encoding="utf-8") as file:
json.dump(d, file) json.dump(d, file)
except OSError as e: except OSError as e:
raise UserFacingError("Failed to save store.") from e raise err.UserFacingError("Failed to save store.") from e
@staticmethod @staticmethod
def restore() -> "Store": def restore() -> "Store":
@@ -219,18 +220,9 @@ class Store:
return store return store
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise UserFacingError("Failed to parse state json.") from e raise err.UserFacingError("Failed to parse state json.") from e
except OSError as e: except OSError as e:
raise UserFacingError("Failed to read saved store.") from e raise err.UserFacingError("Failed to read saved store.") from e
class UserFacingError(Exception):
"""
Execution of an important step failed and the program shouldn't continue.
"""
def __init__(self, user_facing_msg: str):
self.user_facing_msg = user_facing_msg
class Source: class Source:
@@ -310,7 +302,7 @@ class Source:
file.copy_to(target, variables) file.copy_to(target, variables)
print_debug(f"Installing file to {target}.") print_debug(f"Installing file to {target}.")
except OSError as e: except OSError as e:
raise UserFacingError( raise err.UserFacingError(
f"Failed to install file to {target}.") from e f"Failed to install file to {target}.") from e
def install_dirs(dirs: dict[str, decman.Directory], def install_dirs(dirs: dict[str, decman.Directory],
@@ -320,7 +312,7 @@ class Source:
print_debug(f"Installing directory to {target}.") print_debug(f"Installing directory to {target}.")
directory.copy_to(target, variables) directory.copy_to(target, variables)
except OSError as e: except OSError as e:
raise UserFacingError( raise err.UserFacingError(
f"Failed to install directory to {target}.") from e f"Failed to install directory to {target}.") from e
install_files(self.files) install_files(self.files)
@@ -514,7 +506,7 @@ class Pacman:
).stdout.decode().split('\n') ).stdout.decode().split('\n')
return packages return packages
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError( raise err.UserFacingError(
f"Failed to get installed packages using '{error.cmd}'. Output: {error.stdout}." f"Failed to get installed packages using '{error.cmd}'. Output: {error.stdout}."
) from error ) from error
@@ -542,7 +534,7 @@ class Pacman:
check=True, check=True,
stdout=subprocess.PIPE).stdout.decode().strip().split('\n') stdout=subprocess.PIPE).stdout.decode().strip().split('\n')
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError( raise err.UserFacingError(
f"Failed to get foreign packages using '{error.cmd}'. Output: {error.stdout}." f"Failed to get foreign packages using '{error.cmd}'. Output: {error.stdout}."
) from error ) from error
@@ -550,7 +542,7 @@ class Pacman:
return [(line.split(" ")[0], line.split(" ")[1]) return [(line.split(" ")[0], line.split(" ")[1])
for line in output] for line in output]
except IndexError as error: except IndexError as error:
raise UserFacingError( raise err.UserFacingError(
f"Failed to get foreign packages from pacman output. Output: {output}" f"Failed to get foreign packages from pacman output. Output: {output}"
) from error ) from error
@@ -561,7 +553,7 @@ class Pacman:
try: try:
subprocess.run(conf.commands.install_pkgs(packages), check=True) subprocess.run(conf.commands.install_pkgs(packages), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError("Failed to install packages.") from error raise err.UserFacingError("Failed to install packages.") from error
def install_dependencies(self, deps: list[str]): def install_dependencies(self, deps: list[str]):
""" """
@@ -570,7 +562,7 @@ class Pacman:
try: try:
subprocess.run(conf.commands.install_deps(deps), check=True) subprocess.run(conf.commands.install_deps(deps), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError( raise err.UserFacingError(
"Failed to install dependency packages.") from error "Failed to install dependency packages.") from error
def install_files(self, files: list[str], as_explicit: list[str]): def install_files(self, files: list[str], as_explicit: list[str]):
@@ -585,7 +577,7 @@ class Pacman:
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 UserFacingError( raise err.UserFacingError(
"Failed to install foreign packages.") from error "Failed to install foreign packages.") from error
def upgrade(self): def upgrade(self):
@@ -595,7 +587,7 @@ class Pacman:
try: try:
subprocess.run(conf.commands.upgrade(), check=True) subprocess.run(conf.commands.upgrade(), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError("Failed to update packages.") from error raise err.UserFacingError("Failed to update packages.") from error
def remove(self, packages: list[str]): def remove(self, packages: list[str]):
""" """
@@ -604,7 +596,7 @@ class Pacman:
try: try:
subprocess.run(conf.commands.remove(packages), check=True) subprocess.run(conf.commands.remove(packages), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError("Failed to remove packages.") from error raise err.UserFacingError("Failed to remove packages.") from error
class Systemd: class Systemd:
@@ -622,7 +614,8 @@ class Systemd:
try: try:
subprocess.run(conf.commands.enable_units(units), check=True) subprocess.run(conf.commands.enable_units(units), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError("Failed to enable systemd units.") from error raise err.UserFacingError(
"Failed to enable systemd units.") from error
self.state.enabled_systemd_units += units self.state.enabled_systemd_units += units
def disable_units(self, units: list[str]): def disable_units(self, units: list[str]):
@@ -632,7 +625,7 @@ class Systemd:
try: try:
subprocess.run(conf.commands.disable_units(units), check=True) subprocess.run(conf.commands.disable_units(units), check=True)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError( raise err.UserFacingError(
"Failed to disable systemd units.") from error "Failed to disable systemd units.") from error
for unit in units: for unit in units:
try: try:
@@ -649,14 +642,13 @@ class Systemd:
gid = pwd.getpwnam(user).pw_gid gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(conf.commands.enable_user_units(units), with subprocess.Popen(conf.commands.enable_user_units(units),
start_new_session=True,
group=gid, group=gid,
user=uid) as process: user=uid) as process:
if process.wait() != 0: if process.wait() != 0:
raise UserFacingError( raise err.UserFacingError(
f"Failed to enable systemd units for {user}.") f"Failed to enable systemd units for {user}.")
except KeyError as error: except KeyError as error:
raise UserFacingError( raise err.UserFacingError(
f"Failed to enable systemd units because user '{user}' doesn't exist." f"Failed to enable systemd units because user '{user}' doesn't exist."
) from error ) from error
for unit in units: for unit in units:
@@ -671,14 +663,13 @@ class Systemd:
gid = pwd.getpwnam(user).pw_gid gid = pwd.getpwnam(user).pw_gid
with subprocess.Popen(conf.commands.disable_user_units(units), with subprocess.Popen(conf.commands.disable_user_units(units),
start_new_session=True,
group=gid, group=gid,
user=uid) as process: user=uid) as process:
if process.wait() != 0: if process.wait() != 0:
raise UserFacingError( raise err.UserFacingError(
f"Failed to disable systemd units for {user}.") f"Failed to disable systemd units for {user}.")
except KeyError as error: except KeyError as error:
raise UserFacingError( raise err.UserFacingError(
f"Failed to disable systemd units because user '{user}' doesn't exist." f"Failed to disable systemd units because user '{user}' doesn't exist."
) from error ) from error
+17 -16
View File
@@ -22,6 +22,7 @@ import requests
import decman import decman
import decman.config as conf import decman.config as conf
import decman.lib as l import decman.lib as l
import decman.error as err
def strip_dependency(dep: str) -> str: def strip_dependency(dep: str) -> str:
@@ -208,7 +209,7 @@ class DepGraph:
parent_node = self.package_nodes[parent_pkgname] parent_node = self.package_nodes[parent_pkgname]
if parent_node.is_pkgname_in_parents_recursive(child_pkgname): if parent_node.is_pkgname_in_parents_recursive(child_pkgname):
raise l.UserFacingError( raise err.UserFacingError(
f"Foreign package dependency cycle detected involving '{child_pkgname}' \ f"Foreign package dependency cycle detected involving '{child_pkgname}' \
and '{parent_pkgname}'. Foreign package dependencies are also required \ and '{parent_pkgname}'. Foreign package dependencies are also required \
during package building and therefore dependency cycles cannot be handled.") during package building and therefore dependency cycles cannot be handled.")
@@ -292,7 +293,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise l.UserFacingError( raise err.UserFacingError(
f"AUR RPC returned error: {d['error']}") f"AUR RPC returned error: {d['error']}")
for result in d["results"]: for result in d["results"]:
@@ -323,7 +324,7 @@ class ExtendedPackageSearch:
l.print_debug("Request completed.") l.print_debug("Request completed.")
except (requests.RequestException, KeyError) as e: except (requests.RequestException, KeyError) as e:
raise l.UserFacingError( raise err.UserFacingError(
"Failed to fetch package information from AUR RPC.") from e "Failed to fetch package information from AUR RPC.") from e
def get_package_info(self, package: str) -> typing.Optional[PackageInfo]: def get_package_info(self, package: str) -> typing.Optional[PackageInfo]:
@@ -352,7 +353,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise l.UserFacingError( raise err.UserFacingError(
f"AUR RPC returned error: {d['error']}") f"AUR RPC returned error: {d['error']}")
if d["resultcount"] == 0: if d["resultcount"] == 0:
@@ -378,7 +379,7 @@ class ExtendedPackageSearch:
return info return info
except (requests.RequestException, KeyError) as e: except (requests.RequestException, KeyError) as e:
raise l.UserFacingError( raise err.UserFacingError(
"Failed to fetch package information from AUR RPC.") from e "Failed to fetch package information from AUR RPC.") from e
def find_provider( def find_provider(
@@ -432,7 +433,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise l.UserFacingError( raise err.UserFacingError(
f"AUR RPC returned error: {d['error']}") f"AUR RPC returned error: {d['error']}")
if d["resultcount"] == 0: if d["resultcount"] == 0:
@@ -451,7 +452,7 @@ class ExtendedPackageSearch:
return self._choose_provider(stripped_dependency, results, "AUR") return self._choose_provider(stripped_dependency, results, "AUR")
except (requests.RequestException, KeyError) as e: except (requests.RequestException, KeyError) as e:
raise l.UserFacingError( raise err.UserFacingError(
"Failed to fetch package information from AUR RPC.") from e "Failed to fetch package information from AUR RPC.") from e
def _choose_provider(self, dep: str, possible_providers: list[str], def _choose_provider(self, dep: str, possible_providers: list[str],
@@ -568,7 +569,7 @@ class ForeignPackageManager:
info = self._search.get_package_info(pkg) info = self._search.get_package_info(pkg)
if info is None: if info is None:
raise l.UserFacingError(f"Failed to find package: {pkg}.") raise err.UserFacingError(f"Failed to find package: {pkg}.")
if self.should_upgrade_package(pkg, ver, info.version, if self.should_upgrade_package(pkg, ver, info.version,
upgrade_devel): upgrade_devel):
@@ -626,7 +627,7 @@ class ForeignPackageManager:
l.print_continuation("") l.print_continuation("")
if not l.prompt_confirm("Proceed?", default=True): if not l.prompt_confirm("Proceed?", default=True):
raise l.UserFacingError("Installing aborted.") raise err.UserFacingError("Installing aborted.")
l.print_summary("Installing foreign package dependencies from pacman.") l.print_summary("Installing foreign package dependencies from pacman.")
self._pacman.install_dependencies( self._pacman.install_dependencies(
@@ -649,7 +650,7 @@ class ForeignPackageManager:
builder.build_packages(pkgbase, packages, force) builder.build_packages(pkgbase, packages, force)
except (subprocess.CalledProcessError, OSError) as e: except (subprocess.CalledProcessError, OSError) as e:
raise l.UserFacingError("Failed to build packages.") from e raise err.UserFacingError("Failed to build packages.") from e
packages_to_install = list(resolved_dependencies.foreign_pkgs) packages_to_install = list(resolved_dependencies.foreign_pkgs)
packages_to_install += list(resolved_dependencies.foreign_dep_pkgs) packages_to_install += list(resolved_dependencies.foreign_dep_pkgs)
@@ -710,7 +711,7 @@ class ForeignPackageManager:
dep_info = self._search.find_provider(depname) dep_info = self._search.find_provider(depname)
if dep_info is None: if dep_info is None:
raise l.UserFacingError( raise err.UserFacingError(
f"Failed to find '{depname}' from AUR or user provided packages." f"Failed to find '{depname}' from AUR or user provided packages."
) )
@@ -728,7 +729,7 @@ class ForeignPackageManager:
info = self._search.get_package_info(pkgname) info = self._search.get_package_info(pkgname)
if info is None: if info is None:
raise l.UserFacingError( raise err.UserFacingError(
f"Failed to find '{pkgname}' from AUR or user provided packages." f"Failed to find '{pkgname}' from AUR or user provided packages."
) )
@@ -791,7 +792,7 @@ class ForeignPackageManager:
) )
return should_upgrade return should_upgrade
except (ValueError, subprocess.CalledProcessError) as error: except (ValueError, subprocess.CalledProcessError) as error:
raise l.UserFacingError("Failed to compare versions.") from error raise err.UserFacingError("Failed to compare versions.") from error
class PackageBuilder: class PackageBuilder:
@@ -1056,7 +1057,7 @@ class PackageBuilder:
continue continue
if len(matches) != 1: if len(matches) != 1:
raise l.UserFacingError( raise err.UserFacingError(
f"Failed to build package '{pkgname}', because the pkg file cannot be determined." f"Failed to build package '{pkgname}', because the pkg file cannot be determined."
) )
@@ -1096,9 +1097,9 @@ class PackageBuilder:
self._store.pkgbuild_latest_reviewed_commits[ self._store.pkgbuild_latest_reviewed_commits[
pkgbase] = commit_id pkgbase] = commit_id
else: else:
raise l.UserFacingError("Building aborted.") raise err.UserFacingError("Building aborted.")
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise l.UserFacingError( raise err.UserFacingError(
f"Failed to clone and review PKGBUILD from {git_url}" f"Failed to clone and review PKGBUILD from {git_url}"
) from error ) from error
+2 -1
View File
@@ -1,7 +1,8 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
import unittest import unittest
from decman.lib import UserFacingError, Pacman, Store from decman.error import UserFacingError
from decman.lib import Pacman, Store
from decman.lib.aur import ForeignPackageManager, DepGraph, ForeignPackage, ExtendedPackageSearch from decman.lib.aur import ForeignPackageManager, DepGraph, ForeignPackage, ExtendedPackageSearch