Compare commits

...
8 Commits
Author SHA1 Message Date
arcanis 885893ab35 use local devtools configs instead of global ones 2026-08-13 18:15:48 +03:00
arcanis 695424fa16 feat: remove makepkg.conf local configuration, use ahriman own
configuration instead

We used .makepkg.conf solely for two features - MAKEFLAGS and PACKAGER,
which could be actually easily implemented by using environment
variables
2026-08-13 16:39:13 +03:00
arcanis 255ebe117a feat: read includes from list of directories 2026-08-13 16:39:13 +03:00
arcanis a9a12cd247 feat: get rid of sudoers modification during setup command
This commit introduces small shell wrapper, which calls archbuild
mimicing symlinked command. It allows to distribute static sudoers file.

Additionally build_command option has been renamed to devtools_wrapper
in order to allow to keep backward compatibility
2026-08-13 14:26:49 +03:00
arcanis 8dc198aa2b build: tox cleanup 2026-08-13 14:26:49 +03:00
arcanis 1133b75c33 refactor: implement generic optional import mechanisms
this project is actively using optional imports, which leads to
duplicate handling here and there. This commit implements logic in
special method to reduce complexity and simplify tests paths
2026-08-13 14:26:49 +03:00
arcanis 777bedf772 build: handle local imports in pylint check correctly 2026-08-13 14:26:49 +03:00
arcanis cc6ca46342 build: fix mypy type install 2026-08-13 14:26:49 +03:00
42 changed files with 494 additions and 369 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ For installation details kindly refer to the [documentation](https://ahriman.rea
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/stable/configuration.html). Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/stable/configuration.html).
The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions. The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain) are recommended and can be easily achieved by following install instructions.
## [FAQ](https://ahriman.readthedocs.io/en/stable/faq/index.html) ## [FAQ](https://ahriman.readthedocs.io/en/stable/faq/index.html)
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
usage() {
local cmd="${0##*/}"
echo "Usage: $cmd [options] -- [archbuild args]"
echo " -r <repository> Repository name"
echo " -a <architecture> Repository architecture"
echo " -c <directory> Read devtools pacman configurations from this directory"
exit 1
}
repository=
architecture=
pacman_config_dir=
while getopts ":r:a:c:" arg; do
case "$arg" in
r) repository="$OPTARG" ;;
a) architecture="$OPTARG" ;;
c) pacman_config_dir="$OPTARG" ;;
*) usage ;;
esac
done
if [[ -z $repository || -z $architecture ]]; then
usage
fi
source "/usr/share/devtools/lib/archroot.sh"
check_root "SOURCE_DATE_EPOCH,SRCDEST,SRCPKGDEST,PKGDEST,LOGDEST,NPROC,MAKEFLAGS,PACKAGER,GNUPGHOME" "${BASH_SOURCE[0]}" "$@"
# because devtools doesn't allow to read configuration from custom path
# here is a workaround, which uses unshare to bind-mount directory with configuration files
# for specific process
if [[ -n $pacman_config_dir && -z ${AHRIMAN_ARCHBUILD_MOUNTED:-} ]]; then
export AHRIMAN_ARCHBUILD_MOUNTED=1
exec unshare --mount --propagation private bash -e -c '
mount --bind "$0" /usr/share/devtools/pacman.conf.d
exec "$@"
' "$(readlink -f "$pacman_config_dir")" "${BASH_SOURCE[0]}" "$@"
fi
exec bash -c '
source "$1" "${@:2}"
' "${repository}-${architecture}-build" "archbuild" "${@:$OPTIND}"
@@ -1,6 +1,6 @@
[settings] [settings]
; Relative path to directory with configuration files overrides. Overrides will be applied in alphabetic order. ; Relative path to directory with configuration files overrides. Overrides will be applied in alphabetic order.
include = ahriman.ini.d include = ahriman.ini.d ${repository:root}/.config/ahriman/ahriman.ini.d
; Relative path to configuration used by logging package. ; Relative path to configuration used by logging package.
logging = ahriman.ini.d/logging.ini logging = ahriman.ini.d/logging.ini
; Perform database migrations on the application start. Do not touch this option unless you know what you are doing. ; Perform database migrations on the application start. Do not touch this option unless you know what you are doing.
@@ -34,18 +34,24 @@ retry_backoff = 1.0
[build] [build]
; List of additional flags passed to archbuild command. ; List of additional flags passed to archbuild command.
;archbuild_flags = ;archbuild_flags =
; Path to local directory with devtools configuration files, which will be bind-mounted for devtools.
devtools_configs = ${repository:root}/.config/ahriman/pacman.conf.d
; Path to build command. ; Path to build command.
;build_command = devtools_wrapper = ahriman-archbuild
; List of packages to be ignored during automatic updates. ; List of packages to be ignored during automatic updates.
;ignore_packages = ;ignore_packages =
; Include debug packages. ; Include debug packages.
;include_debug_packages = yes ;include_debug_packages = yes
; List of additional flags passed to make via environment variable via makepkg command.
;make_flags =
; List of additional flags passed to makechrootpkg command. ; List of additional flags passed to makechrootpkg command.
;makechrootpkg_flags = ;makechrootpkg_flags =
; Minimal age in seconds since the latest AUR package modification before automatic updates are allowed.
;min_age = 0
; List of additional flags passed to makepkg command. ; List of additional flags passed to makepkg command.
makepkg_flags = --nocolor --ignorearch makepkg_flags = --nocolor --ignorearch
; Minimal age in seconds since the latest AUR package modification before automatic updates are allowed.
;min_age = 0
; Default packager identifier to be used for package builds
;packager =
; List of paths to be used for implicit dependency scan. Regular expressions are supported. ; List of paths to be used for implicit dependency scan. Regular expressions are supported.
scan_paths = ^usr/lib(?!/cmake).*$ scan_paths = ^usr/lib(?!/cmake).*$
; List of enabled triggers in the order of calls. ; List of enabled triggers in the order of calls.
+1
View File
@@ -57,6 +57,7 @@ packages = [
] ]
[tool.hatch.build.targets.wheel.shared-data] [tool.hatch.build.targets.wheel.shared-data]
"package/bin" = "bin"
"package/lib" = "lib" "package/lib" = "lib"
"package/share" = "share" "package/share" = "share"
@@ -18,19 +18,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import multiprocessing
import os
from pathlib import Path from pathlib import Path
from pwd import getpwuid
from typing import ClassVar from typing import ClassVar
from urllib.parse import quote_plus as url_encode from urllib.parse import quote_plus as url_encode
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitectureError from ahriman.core.exceptions import InitializeError, MissingArchitectureError
from ahriman.core.utils import enum_values from ahriman.core.utils import enum_values
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
from ahriman.models.user import User from ahriman.models.user import User
@@ -40,16 +40,12 @@ class Setup(Handler):
setup handler setup handler
Attributes: Attributes:
ARCHBUILD_COMMAND_PATH(Path): (class attribute) default devtools command
MIRRORLIST_PATH(Path): (class attribute) path to pacman default mirrorlist (used by multilib repository) MIRRORLIST_PATH(Path): (class attribute) path to pacman default mirrorlist (used by multilib repository)
SUDOERS_DIR_PATH(Path): (class attribute) path to sudoers.d includes directory
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist" MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
@@ -67,21 +63,20 @@ class Setup(Handler):
if args.architecture is None or args.repository is None: if args.architecture is None or args.repository is None:
raise MissingArchitectureError(args.command) raise MissingArchitectureError(args.command)
Setup.configuration_create_ahriman(args, repository_id, configuration) target_directory = Setup.configuration_create_directory(configuration)
Setup.configuration_create_ahriman(args, repository_id, configuration, target_directory)
configuration.reload() configuration.reload()
application = Application(repository_id, configuration, report=report) application = Application(repository_id, configuration, report=report)
paths = application.repository.paths
# basically we create configuration here as root, but it is ok, because those files are only used for reading repository_server = f"file://{paths.repository}" if args.server is None else args.server
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths) target_directory = paths.ensure_exists(configuration.getpath("build", "devtools_configs"))
Setup.executable_create(application.repository.paths, repository_id) Setup.configuration_create_devtools(repository_id, args.from_configuration, target_directory, args.mirror,
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server args.multilib, repository_server)
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
# finish initialization # finish initialization
with application.repository.paths.preserve_owner(): with paths.preserve_owner():
application.repository.repo.init() application.repository.repo.init()
# lazy database sync # lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement application.repository.pacman.handle # pylint: disable=pointless-statement
@@ -124,23 +119,9 @@ class Setup(Handler):
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True) parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
return parser return parser
@staticmethod
def build_command(root: Path, repository_id: RepositoryId) -> Path:
"""
generate build command name
Args:
root(Path): root directory for the build command (must be root of the repository)
repository_id(RepositoryId): repository unique identifier
Returns:
Path: valid devtools command name
"""
return root / f"{repository_id.name}-{repository_id.architecture}-build"
@staticmethod @staticmethod
def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId, def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId,
root: Configuration) -> None: root: Configuration, target_directory: Path) -> None:
""" """
create service specific configuration create service specific configuration
@@ -148,13 +129,16 @@ class Setup(Handler):
args(argparse.Namespace): command line args args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
root(Configuration): root configuration instance root(Configuration): root configuration instance
target_directory(Path): path to directory where configuration files will be written
""" """
configuration = Configuration() configuration = Configuration()
section = Configuration.section_name("build", repository_id.name, repository_id.architecture)
build_command = Setup.build_command(root.repository_paths.root, repository_id)
configuration.set_option(section, "build_command", str(build_command))
configuration.set_option("repository", "name", repository_id.name) # backward compatibility for docker configuration.set_option("repository", "name", repository_id.name) # backward compatibility for docker
section = Configuration.section_name("build", repository_id.name, repository_id.architecture)
configuration.set_option(section, "packager", args.packager)
if args.makeflags_jobs:
configuration.set_option(section, "make_flags", f"-j{multiprocessing.cpu_count()}")
if args.build_as_user is not None: if args.build_as_user is not None:
configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}") configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}")
@@ -183,14 +167,13 @@ class Setup(Handler):
if args.generate_salt: if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20)) configuration.set_option("auth", "salt", User.generate_password(20))
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration target = target_directory / f"00-setup-overrides-{repository_id.id}.ini"
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w", encoding="utf8") as ahriman_configuration: with target.open("w", encoding="utf8") as ahriman_configuration:
configuration.write(ahriman_configuration) configuration.write(ahriman_configuration)
@staticmethod @staticmethod
def configuration_create_devtools(repository_id: RepositoryId, source: Path, mirror: str | None, def configuration_create_devtools(repository_id: RepositoryId, source: Path, target_directory: Path,
multilib: bool, repository_server: str) -> None: mirror: str | None, multilib: bool, repository_server: str) -> None:
""" """
create configuration for devtools based on ``source`` configuration create configuration for devtools based on ``source`` configuration
@@ -200,6 +183,7 @@ class Setup(Handler):
Args: Args:
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
source(Path): path to source configuration file source(Path): path to source configuration file
target_directory(Path): path to directory where configuration files will be written
mirror(str | None): link to package server mirror mirror(str | None): link to package server mirror
multilib(bool): add or do not multilib repository to the configuration multilib(bool): add or do not multilib repository to the configuration
repository_server(str): url of the repository repository_server(str): url of the repository
@@ -234,54 +218,32 @@ class Setup(Handler):
configuration.set_option(repository_id.name, "SigLevel", "Never") # we don't care configuration.set_option(repository_id.name, "SigLevel", "Never") # we don't care
configuration.set_option(repository_id.name, "Server", repository_server) configuration.set_option(repository_id.name, "Server", repository_server)
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf" target = target_directory / f"{repository_id.name}-{repository_id.architecture}.conf"
with target.open("w", encoding="utf8") as devtools_configuration: with target.open("w", encoding="utf8") as devtools_configuration:
configuration.write(devtools_configuration) configuration.write(devtools_configuration)
@staticmethod @staticmethod
def configuration_create_makepkg(packager: str, makeflags_jobs: bool, paths: RepositoryPaths) -> None: def configuration_create_directory(root: Configuration) -> Path:
""" """
create configuration for makepkg create directory for includes
Args: Args:
packager(str): packager identifier (e.g. name, email) root(Configuration): root configuration instance
makeflags_jobs(bool): set MAKEFLAGS variable to number of cores
paths(RepositoryPaths): repository paths instance Returns:
Path: path to first writable directory
Raises:
InitializeError: if no writable directories have been found
""" """
for include_path in root.getpathlist("settings", "include"):
try:
directory = root.repository_paths.ensure_exists(include_path)
if os.access(directory, os.W_OK | os.X_OK):
return directory
except OSError:
continue
content = f"PACKAGER='{packager}'\n" raise InitializeError("No writable include directory found")
if makeflags_jobs:
content += "MAKEFLAGS=\"-j$(nproc)\"\n"
uid, _ = paths.root_owner
home_dir = Path(getpwuid(uid).pw_dir)
(home_dir / ".makepkg.conf").write_text(content, encoding="utf8")
@staticmethod
def configuration_create_sudo(paths: RepositoryPaths, repository_id: RepositoryId) -> None:
"""
create configuration to run build command with sudo without password
Args:
paths(RepositoryPaths): repository paths instance
repository_id(RepositoryId): repository unique identifier
"""
command = Setup.build_command(paths.root, repository_id)
sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, repository_id)
sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8")
sudoers_file.chmod(0o400) # security!
@staticmethod
def executable_create(paths: RepositoryPaths, repository_id: RepositoryId) -> None:
"""
create executable for the service
Args:
paths(RepositoryPaths): repository paths instance
repository_id(RepositoryId): repository unique identifier
"""
command = Setup.build_command(paths.root, repository_id)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
arguments = [_set_service_setup_parser] arguments = [_set_service_setup_parser]
@@ -56,7 +56,7 @@ class PackageVersion(LazyLogging):
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
paths = configuration.repository_paths paths = configuration.repository_paths
task = Task(self.package, configuration, repository_id.architecture, paths) task = Task(self.package, configuration, repository_id, paths)
try: try:
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
@@ -27,6 +27,7 @@ from ahriman.core.log import LazyLogging
from ahriman.core.utils import check_output, package_like from ahriman.core.utils import check_output, package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@@ -36,33 +37,40 @@ class Task(LazyLogging):
Attributes: Attributes:
archbuild_flags(list[str]): command flags for archbuild command archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture build_command(list[str]): build command
build_command(str): build command devtools_configs(Path): path to local directory with devtools configuration files
include_debug_packages(bool): whether to include debug packages or not include_debug_packages(bool): whether to include debug packages or not
make_flags(str): MAKEFLAGS variable for makepkg command
makechrootpkg_flags(list[str]): command flags for makechrootpkg command makechrootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions package(Package): package definitions
paths(RepositoryPaths): repository paths instance paths(RepositoryPaths): repository paths instance
repository_id(RepositoryId): repository unique identifier
uid(int): uid of the repository owner user uid(int): uid of the repository owner user
""" """
def __init__(self, package: Package, configuration: Configuration, architecture: str, def __init__(self, package: Package, configuration: Configuration, repository_id: RepositoryId,
paths: RepositoryPaths) -> None: paths: RepositoryPaths) -> None:
""" """
Args: Args:
package(Package): package definitions package(Package): package definitions
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
architecture(str): repository architecture repository_id(RepositoryId): repository unique identifier
paths(RepositoryPaths): repository paths instance paths(RepositoryPaths): repository paths instance
""" """
self.package = package self.package = package
self.paths = paths self.paths = paths
self.uid, _ = paths.root_owner self.uid, _ = paths.root_owner
self.architecture = architecture self.repository_id = repository_id
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[]) self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command") self.build_command = configuration.getlist("build", "devtools_wrapper")
self.devtools_configs = configuration.getpath("build", "devtools_configs")
self._legacy_build_command = configuration.getlist("build", "build_command", fallback=[])
self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True) self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True)
# even though this option is declared as list, there is no need to read it as list,
# because it will be converted back to the string anyway
self.make_flags = configuration.get("build", "make_flags", fallback=None)
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
@@ -108,12 +116,21 @@ class Task(LazyLogging):
Returns: Returns:
list[Path]: paths of produced packages list[Path]: paths of produced packages
""" """
command = [self.build_command, "-r", str(self.paths.chroot)] command = self._legacy_build_command[:]
command.extend(self.archbuild_flags) if not command:
command.extend(["--", "-D", str(self.paths.archive)] + self.makechrootpkg_flags) command = self.build_command + [
command.extend(["--"] + self.makepkg_flags) "-r", self.repository_id.name,
"-a", self.repository_id.architecture,
"-c", str(self.devtools_configs),
"--",
]
command.extend(["-r", str(self.paths.chroot)] + self.archbuild_flags) # archbuild flags
command.extend(["--", "-D", str(self.paths.archive)] + self.makechrootpkg_flags) # makechrootpkg flags
command.extend(["--"] + self.makepkg_flags) # makepkg flags
if dry_run: if dry_run:
command.extend(["--nobuild"]) command.extend(["--nobuild"])
self.logger.info("using %s for %s", command, self.package.base) self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = { environment: dict[str, str] = {
@@ -121,6 +138,8 @@ class Task(LazyLogging):
for key, value in kwargs.items() for key, value in kwargs.items()
if value is not None if value is not None
} }
if self.make_flags is not None:
environment["MAKEFLAGS"] = self.make_flags
self.logger.info("using environment variables %s", environment) self.logger.info("using environment variables %s", environment)
source_files = list(sources_dir.iterdir()) source_files = list(sources_dir.iterdir())
@@ -156,7 +175,7 @@ class Task(LazyLogging):
return last_commit_sha return last_commit_sha
# load fresh package # load fresh package
loaded_package = Package.from_build(sources_dir, self.architecture, None) loaded_package = Package.from_build(sources_dir, self.repository_id.architecture, None)
if (pkgrel := loaded_package.next_pkgrel(local_version)) is not None: if (pkgrel := loaded_package.next_pkgrel(local_version)) is not None:
self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel) self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
patch = PkgbuildPatch("pkgrel", pkgrel) patch = PkgbuildPatch("pkgrel", pkgrel)
@@ -108,16 +108,6 @@ class Configuration(configparser.RawConfigParser):
_, repository_id = self.check_loaded() _, repository_id = self.check_loaded()
return repository_id.architecture return repository_id.architecture
@property
def include(self) -> Path:
"""
get full path to include directory
Returns:
Path: path to directory with configuration includes
"""
return self.getpath("settings", "include")
@property @property
def logging_path(self) -> Path: def logging_path(self) -> Path:
""" """
@@ -325,25 +315,35 @@ class Configuration(configparser.RawConfigParser):
section, key = name.rsplit(":", maxsplit=1) section, key = name.rsplit(":", maxsplit=1)
self.set_option(section, key, value) self.set_option(section, key, value)
def load_includes(self, path: Path | None = None) -> None: def load_includes(self) -> None:
""" """
load configuration includes from specified path load configuration includes from specified path
Args:
path(Path | None, optional): path to directory with include files. If none set, the default path will be
used (Default value = None)
""" """
self.includes = [] # reset state self.includes = [] # reset state
try: try:
path = path or self.include # raw processing to make sure that options are applied correctly
include_directories = shlex.split(self.get("settings", "include", raw=True))
except (configparser.NoOptionError, configparser.NoSectionError):
return
for directory in include_directories:
value = self._interpolation.before_get( # type: ignore[attr-defined]
self,
"settings",
"include",
directory,
self._unify_values("settings", None), # type: ignore[attr-defined]
)
path = self._convert_path(value)
if not path.is_dir():
continue
for include in sorted(path.glob("*.ini")): for include in sorted(path.glob("*.ini")):
if include == self.logging_path: if include == self.logging_path:
continue # we don't want to load logging explicitly continue # we don't want to load logging explicitly
self.read(include) self.read(include)
self.includes.append(include) self.includes.append(include)
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass
def merge_sections(self, repository_id: RepositoryId) -> None: def merge_sections(self, repository_id: RepositoryId) -> None:
""" """
@@ -403,6 +403,7 @@ class Configuration(configparser.RawConfigParser):
# create another instance and copy values from there # create another instance and copy values from there
instance = self.from_path(path, repository_id) instance = self.from_path(path, repository_id)
self.copy_from(instance) self.copy_from(instance)
self.includes = instance.includes
def set_option(self, section: str, option: str, value: str) -> None: def set_option(self, section: str, option: str, value: str) -> None:
""" """
@@ -40,11 +40,14 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"required": True, "required": True,
}, },
"include": { "include": {
"type": "list",
"coerce": "list",
"schema": {
"type": "path", "type": "path",
"coerce": "absolute_path", "coerce": "absolute_path",
"path_exists": True,
"path_type": "dir", "path_type": "dir",
}, },
},
"logging": { "logging": {
"type": "path", "type": "path",
"coerce": "absolute_path", "coerce": "absolute_path",
@@ -190,8 +193,27 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
}, },
}, },
"build_command": { "build_command": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string", "type": "string",
"empty": False,
},
},
"devtools_configs": {
"type": "path",
"coerce": "absolute_path",
"required": True, "required": True,
"path_exists": True,
"path_type": "dir",
},
"devtools_wrapper": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
"empty": False, "empty": False,
}, },
"ignore_packages": { "ignore_packages": {
@@ -206,6 +228,14 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"type": "boolean", "type": "boolean",
"coerce": "boolean", "coerce": "boolean",
}, },
"make_flags": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"makechrootpkg_flags": { "makechrootpkg_flags": {
"type": "list", "type": "list",
"coerce": "list", "coerce": "list",
@@ -214,19 +244,23 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False, "empty": False,
}, },
}, },
"makepkg_flags": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"min_age": { "min_age": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0, "min": 0,
}, },
"makepkg_flags": { "packager": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string", "type": "string",
"empty": False, "empty": False,
}, },
},
"scan_paths": { "scan_paths": {
"type": "list", "type": "list",
"coerce": "list", "coerce": "list",
@@ -190,5 +190,5 @@ class Validator(RootValidator):
{"type": "string"} {"type": "string"}
""" """
fn = getattr(value, f"is_{constraint}") fn = getattr(value, f"is_{constraint}")
if not fn(): if value.exists() and not fn():
self._error(field, f"Path {value} must be type of {constraint}") self._error(field, f"Path {value} must be type of {constraint}")
@@ -21,6 +21,8 @@
from logging import NullHandler from logging import NullHandler
from typing import Any from typing import Any
from ahriman.core.module_loader import optional_module
__all__ = ["JournalHandler"] __all__ = ["JournalHandler"]
@@ -40,7 +42,5 @@ class _JournalHandler(NullHandler):
del args, kwargs del args, kwargs
try: systemd_journal = optional_module("systemd.journal")
from systemd.journal import JournalHandler # type: ignore[import-untyped] JournalHandler = systemd_journal.JournalHandler if systemd_journal else _JournalHandler
except ImportError:
JournalHandler = _JournalHandler
@@ -26,6 +26,7 @@ from typing import ClassVar, Literal
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler from ahriman.core.log.http_log_handler import HttpLogHandler
from ahriman.core.log.log_context import LogContext from ahriman.core.log.log_context import LogContext
from ahriman.core.module_loader import optional_module
from ahriman.models.log_handler import LogHandler from ahriman.models.log_handler import LogHandler
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@@ -65,11 +66,8 @@ class LogLoader:
if selected is not None: if selected is not None:
return selected return selected
try: if optional_module("systemd.journal"):
from systemd.journal import JournalHandler # type: ignore[import-untyped]
del JournalHandler
return LogHandler.Journald # journald import was found return LogHandler.Journald # journald import was found
except ImportError:
if LogLoader.DEFAULT_SYSLOG_DEVICE.exists(): if LogLoader.DEFAULT_SYSLOG_DEVICE.exists():
return LogHandler.Syslog return LogHandler.Syslog
return LogHandler.Console return LogHandler.Console
+17 -2
View File
@@ -26,8 +26,7 @@ from pkgutil import ModuleInfo, walk_packages
from types import ModuleType from types import ModuleType
from typing import Any, TypeGuard, TypeVar from typing import Any, TypeGuard, TypeVar
__all__ = ["implementations", "optional_module"]
__all__ = ["implementations"]
T = TypeVar("T") T = TypeVar("T")
@@ -74,3 +73,19 @@ def implementations(root_module: ModuleType, base_class: type[T]) -> Iterator[ty
for _, attribute in inspect.getmembers(module, is_base_class): for _, attribute in inspect.getmembers(module, is_base_class):
yield attribute yield attribute
def optional_module(module_name: str) -> ModuleType | None:
"""
import an optional module
Args:
module_name(str): fully qualified module name
Returns:
ModuleType | None: imported module or ``None`` when it cannot be imported
"""
try:
return import_module(module_name)
except ImportError:
return None
@@ -73,7 +73,8 @@ class Executor(PackageInfo, Cleaner):
""" """
self.reporter.set_building(package.base) self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.repository_id.architecture, self.paths) default_packager = self.configuration.get("build", "packager", fallback=None)
task = Task(package, self.configuration, self.repository_id, self.paths)
patches = self.reporter.package_patches_get(package.base, None) patches = self.reporter.package_patches_get(package.base, None)
commit_sha = task.init(path, patches, local_version) commit_sha = task.init(path, patches, local_version)
@@ -86,7 +87,7 @@ class Executor(PackageInfo, Cleaner):
shutil.copy(artifact, path) shutil.copy(artifact, path)
built.append(path / artifact.name) built.append(path / artifact.name)
else: else:
built = task.build(path, PACKAGER=packager) built = task.build(path, PACKAGER=packager or default_packager)
package.with_packages(built) package.with_packages(built)
for src in built: for src in built:
@@ -103,7 +103,7 @@ class RepositoryPaths(LazyLogging):
Returns: Returns:
Path: path to directory in which build process is run Path: path to directory in which build process is run
""" """
uid, _ = owner(self.root) uid, _ = self.root_owner
return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name
@property @property
@@ -127,6 +127,16 @@ class RepositoryPaths(LazyLogging):
# for the chroot directory devtools will create own tree, and we don't have to specify architecture here # for the chroot directory devtools will create own tree, and we don't have to specify architecture here
return self.root / "chroot" / self.repository_id.name return self.root / "chroot" / self.repository_id.name
@property
def configs(self) -> Path:
"""
get directory for local configuration files
Returns:
Path: full path to local configuration directory
"""
return self.root / ".config" / "ahriman"
@property @property
def packages(self) -> Path: def packages(self) -> Path:
""" """
@@ -323,6 +333,7 @@ class RepositoryPaths(LazyLogging):
self.archive, self.archive,
self.cache, self.cache,
self.chroot, self.chroot,
self.configs,
self.packages, self.packages,
self.pacman, self.pacman,
self.repository, self.repository,
@@ -1,4 +1,6 @@
import argparse import argparse
import multiprocessing
import os
import pytest import pytest
from pathlib import Path from pathlib import Path
@@ -10,9 +12,8 @@ from urllib.parse import quote_plus as url_encode
from ahriman.application.handlers.setup import Setup from ahriman.application.handlers.setup import Setup
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.core.exceptions import MissingArchitectureError from ahriman.core.exceptions import InitializeError, MissingArchitectureError
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@@ -50,25 +51,29 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
must run command must run command
""" """
args = _default_args(args) args = _default_args(args)
local = Path("local")
mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mkdir_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_directory",
return_value=local)
ahriman_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman") ahriman_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools") devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools")
makepkg_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_makepkg")
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init") init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner") owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False) Setup.run(args, repository_id, configuration, report=False)
owner_guard_mock.assert_called_once_with() owner_guard_mock.assert_called_once_with()
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration) mkdir_mock.assert_called_once_with(configuration)
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration, local)
devtools_configuration_mock.assert_called_once_with( devtools_configuration_mock.assert_called_once_with(
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}") repository_id,
makepkg_configuration_mock.assert_called_once_with(args.packager, args.makeflags_jobs, repository_paths) args.from_configuration,
sudo_configuration_mock.assert_called_once_with(repository_paths, repository_id) configuration.getpath("build", "devtools_configs"),
executable_mock.assert_called_once_with(repository_paths, repository_id) args.mirror,
args.multilib,
f"file://{repository_paths.repository}",
)
init_mock.assert_called_once_with() init_mock.assert_called_once_with()
@@ -101,31 +106,23 @@ def test_run_with_server(args: argparse.Namespace, configuration: Configuration,
mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman") mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman")
mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_makepkg")
mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
mocker.patch("ahriman.core.alpm.repo.Repo.init") mocker.patch("ahriman.core.alpm.repo.Repo.init")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools") devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False) Setup.run(args, repository_id, configuration, report=False)
devtools_configuration_mock.assert_called_once_with( devtools_configuration_mock.assert_called_once_with(
repository_id, args.from_configuration, args.mirror, args.multilib, "server") repository_id,
args.from_configuration,
configuration.getpath("build", "devtools_configs"),
args.mirror,
args.multilib,
"server",
)
def test_build_command(repository_id: RepositoryId) -> None: def test_configuration_create_ahriman(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
""" mocker: MockerFixture) -> None:
must generate correct build command name
"""
path = Path("local")
build_command = Setup.build_command(path, repository_id)
assert build_command.name == f"{repository_id.name}-{repository_id.architecture}-build"
assert build_command.parent == path
def test_configuration_create_ahriman(args: argparse.Namespace, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
""" """
must create configuration for the service must create configuration for the service
""" """
@@ -133,23 +130,23 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
remove_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
command = Setup.build_command(repository_paths.root, repository_id)
Setup.configuration_create_ahriman(args, repository_id, configuration) Setup.configuration_create_ahriman(args, repository_id, configuration, tmp_path)
set_option_mock.assert_has_calls([ set_option_mock.assert_has_calls([
MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture), "build_command",
str(command)),
MockCall("repository", "name", repository_id.name), MockCall("repository", "name", repository_id.name),
MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture),
"packager", args.packager),
MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture),
"make_flags", f"-j{multiprocessing.cpu_count()}"),
MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture), MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture),
"makechrootpkg_flags", f"-U {args.build_as_user}"), "makechrootpkg_flags", f"-U {args.build_as_user}"),
MockCall(Configuration.section_name( MockCall(Configuration.section_name("alpm", repository_id.name, repository_id.architecture),
"alpm", repository_id.name, repository_id.architecture), "mirror", args.mirror), "mirror", args.mirror),
MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "target", MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture),
" ".join([target.name.lower() for target in args.sign_target])), "target", " ".join([target.name.lower() for target in args.sign_target])),
MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "key", MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture),
args.sign_key), "key", args.sign_key),
MockCall("web", "port", str(args.web_port)), MockCall("web", "port", str(args.web_port)),
MockCall("status", "address", f"http://127.0.0.1:{str(args.web_port)}"), MockCall("status", "address", f"http://127.0.0.1:{str(args.web_port)}"),
MockCall("web", "unix_socket", str(args.web_unix_socket)), MockCall("web", "unix_socket", str(args.web_unix_socket)),
@@ -157,11 +154,10 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)), MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
]) ])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
remove_mock.assert_called_once_with(configuration.include / "00-setup-overrides.ini", missing_ok=True)
def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, configuration: Configuration, def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None: tmp_path: Path, mocker: MockerFixture) -> None:
""" """
must create configuration for the service without multilib repository must create configuration for the service without multilib repository
""" """
@@ -172,14 +168,14 @@ def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, conf
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.configuration_create_ahriman(args, repository_id, configuration) Setup.configuration_create_ahriman(args, repository_id, configuration, tmp_path)
set_option_mock.assert_has_calls([ set_option_mock.assert_has_calls([
MockCall(Configuration.section_name("alpm", repository_id.name, repository_id.architecture), "mirror", MockCall(Configuration.section_name("alpm", repository_id.name, repository_id.architecture), "mirror",
args.mirror), args.mirror),
]) # non-strict check called intentionally ]) # non-strict check called intentionally
def test_configuration_create_devtools(args: argparse.Namespace, configuration: Configuration, def test_configuration_create_devtools(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must create configuration for the devtools must create configuration for the devtools
@@ -191,12 +187,12 @@ def test_configuration_create_devtools(args: argparse.Namespace, configuration:
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(repository_id, args.from_configuration, None, args.multilib, "server") Setup.configuration_create_devtools(repository_id, args.from_configuration, tmp_path, None, args.multilib, "server")
add_section_mock.assert_has_calls([MockCall("multilib"), MockCall(repository_id.name)]) add_section_mock.assert_has_calls([MockCall("multilib"), MockCall(repository_id.name)])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_configuration_create_devtools_mirror(args: argparse.Namespace, configuration: Configuration, def test_configuration_create_devtools_mirror(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must create configuration for the devtools with mirror set explicitly must create configuration for the devtools with mirror set explicitly
@@ -216,14 +212,21 @@ def test_configuration_create_devtools_mirror(args: argparse.Namespace, configur
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, args.multilib, "server") Setup.configuration_create_devtools(
repository_id,
args.from_configuration,
tmp_path,
args.mirror,
args.multilib,
"server",
)
get_mock.assert_has_calls([MockCall("core", "Include", fallback=None), MockCall("extra", "Include", fallback=None)]) get_mock.assert_has_calls([MockCall("core", "Include", fallback=None), MockCall("extra", "Include", fallback=None)])
remove_option_mock.assert_called_once_with("core", "Include") remove_option_mock.assert_called_once_with("core", "Include")
set_option_mock.assert_has_calls([MockCall("core", "Server", args.mirror)]) # non-strict check called intentionally set_option_mock.assert_has_calls([MockCall("core", "Server", args.mirror)]) # non-strict check called intentionally
def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, configuration: Configuration, def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None: tmp_path: Path, mocker: MockerFixture) -> None:
""" """
must create configuration for the devtools without multilib must create configuration for the devtools without multilib
""" """
@@ -233,50 +236,31 @@ def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, con
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write") write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, False, "server") Setup.configuration_create_devtools(repository_id, args.from_configuration, tmp_path, args.mirror, False, "server")
write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_configuration_create_makepkg(args: argparse.Namespace, repository_paths: RepositoryPaths, def test_configuration_create_directory(configuration: Configuration, mocker: MockerFixture) -> None:
passwd: Any, mocker: MockerFixture) -> None:
""" """
must create makepkg configuration must create writable directory for includes
""" """
args = _default_args(args) configuration.set_option("settings", "include", f"/path1 /path2 /path3")
mocker.patch("ahriman.application.handlers.setup.getpwuid", return_value=passwd) mkdir_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.ensure_exists",
write_text_mock = mocker.patch("pathlib.Path.write_text", autospec=True) side_effect=[OSError, "/path2", "/path3"])
access_mock = mocker.patch("os.access", side_effect=[False, True])
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, repository_paths) assert Setup.configuration_create_directory(configuration) == "/path3"
write_text_mock.assert_called_once_with( mkdir_mock.assert_has_calls([MockCall(Path("/path1")), MockCall(Path("/path2")), MockCall(Path("/path3"))])
Path("home") / ".makepkg.conf", pytest.helpers.anyvar(str, True), encoding="utf8") access_mock.assert_has_calls([MockCall("/path2", os.W_OK | os.X_OK), MockCall("/path3", os.W_OK | os.X_OK)])
def test_configuration_create_sudo(configuration: Configuration, repository_paths: RepositoryPaths, def test_configuration_create_directory_no_writable(configuration: Configuration, mocker: MockerFixture) -> None:
mocker: MockerFixture) -> None:
""" """
must create sudo configuration must raise InitializeError if no writable directories found
""" """
chmod_text_mock = mocker.patch("pathlib.Path.chmod") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.ensure_exists", side_effect=OSError)
write_text_mock = mocker.patch("pathlib.Path.write_text") with pytest.raises(InitializeError):
Setup.configuration_create_directory(configuration)
_, repository_id = configuration.check_loaded()
Setup.configuration_create_sudo(repository_paths, repository_id)
chmod_text_mock.assert_called_once_with(0o400)
write_text_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), encoding="utf8")
def test_executable_create(configuration: Configuration, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create executable
"""
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
unlink_mock = mocker.patch("pathlib.Path.unlink")
_, repository_id = configuration.check_loaded()
Setup.executable_create(repository_paths, repository_id)
symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH)
unlink_mock.assert_called_once_with(missing_ok=True)
def test_disallow_multi_architecture_run() -> None: def test_disallow_multi_architecture_run() -> None:
@@ -63,7 +63,7 @@ def test_run_default(args: argparse.Namespace, configuration: Configuration) ->
default = Configuration.from_path(Configuration.SYSTEM_CONFIGURATION_PATH, repository_id) default = Configuration.from_path(Configuration.SYSTEM_CONFIGURATION_PATH, repository_id)
# copy autogenerated values # copy autogenerated values
for section, key in (("build", "build_command"), ("repository", "root")): for section, key in (("repository", "root"), ("build", "devtools_configs")):
value = configuration.get(section, key) value = configuration.get(section, key)
default.set_option(section, key, value) default.set_option(section, key, value)
@@ -53,8 +53,11 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
assert task_ahriman.build(local) == [task_ahriman.package.base] assert task_ahriman.build(local) == [task_ahriman.package.base]
check_output_mock.assert_called_once_with( check_output_mock.assert_called_once_with(
"extra-x86_64-build", "ahriman-archbuild",
"-r", str(task_ahriman.paths.chroot), "-r", task_ahriman.repository_id.name,
"-a", task_ahriman.repository_id.architecture,
"-c", str(task_ahriman.devtools_configs),
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive), "--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck", "--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int), exception=pytest.helpers.anyvar(int),
@@ -79,8 +82,11 @@ def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
task_ahriman.build(local, **environment, empty=None) task_ahriman.build(local, **environment, empty=None)
check_output_mock.assert_called_once_with( check_output_mock.assert_called_once_with(
"extra-x86_64-build", "ahriman-archbuild",
"-r", str(task_ahriman.paths.chroot), "-r", task_ahriman.repository_id.name,
"-a", task_ahriman.repository_id.architecture,
"-c", str(task_ahriman.devtools_configs),
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive), "--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck", "--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int), exception=pytest.helpers.anyvar(int),
@@ -91,6 +97,33 @@ def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
) )
def test_build_makeflags(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must build package with MAKEFLAGS variable if set
"""
local = Path("local")
mocker.patch("pathlib.Path.iterdir", return_value=["file"])
mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base])
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
task_ahriman.make_flags = "-j1"
task_ahriman.build(local)
check_output_mock.assert_called_once_with(
"ahriman-archbuild",
"-r", task_ahriman.repository_id.name,
"-a", task_ahriman.repository_id.architecture,
"-c", str(task_ahriman.devtools_configs),
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
user=task_ahriman.uid,
environment={"MAKEFLAGS": "-j1"},
)
def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None: def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None:
""" """
must run devtools in dry-run mode must run devtools in dry-run mode
@@ -100,13 +133,40 @@ def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None:
mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base]) mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base])
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
assert task_ahriman.build(local, dry_run=True) == [task_ahriman.package.base] task_ahriman.build(local, dry_run=True)
check_output_mock.assert_called_once_with(
"ahriman-archbuild",
"-r", task_ahriman.repository_id.name,
"-a", task_ahriman.repository_id.architecture,
"-c", str(task_ahriman.devtools_configs),
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
"--nobuild",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
user=task_ahriman.uid,
environment={},
)
def test_build_legacy_subcommand(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must build package by using legacy subcommand if set
"""
local = Path("local")
mocker.patch("pathlib.Path.iterdir", return_value=["file"])
mocker.patch("ahriman.core.build_tools.task.Task._package_archives", return_value=[task_ahriman.package.base])
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
task_ahriman._legacy_build_command = ["extra-x86_64-build"]
task_ahriman.build(local)
check_output_mock.assert_called_once_with( check_output_mock.assert_called_once_with(
"extra-x86_64-build", "extra-x86_64-build",
"-r", str(task_ahriman.paths.chroot), "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive), "--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck", "--", "--skippgpcheck",
"--nobuild",
exception=pytest.helpers.anyvar(int), exception=pytest.helpers.anyvar(int),
cwd=local, cwd=local,
logger=task_ahriman.logger, logger=task_ahriman.logger,
@@ -374,17 +374,16 @@ def test_load_environment(configuration: Configuration) -> None:
assert configuration.get("section:identifier", "key") == "value2" assert configuration.get("section:identifier", "key") == "value2"
def test_load_includes(mocker: MockerFixture) -> None: def test_load_includes(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must load includes must load includes
""" """
mocker.patch.object(Configuration, "logging_path", Path("logging")) mocker.patch.object(Configuration, "logging_path", Path("logging"))
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read") read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
glob_mock = mocker.patch("pathlib.Path.glob", autospec=True, return_value=[Path("include"), Path("logging")]) glob_mock = mocker.patch("pathlib.Path.glob", autospec=True, return_value=[Path("include"), Path("logging")])
configuration = Configuration()
configuration.load_includes(Path("path")) configuration.load_includes()
glob_mock.assert_called_once_with(Path("path"), "*.ini") glob_mock.assert_called_once_with(configuration.path.absolute().parent, "*.ini")
read_mock.assert_called_once_with(Path("include")) read_mock.assert_called_once_with(Path("include"))
assert configuration.includes == [Path("include")] assert configuration.includes == [Path("include")]
@@ -415,17 +414,6 @@ def test_load_includes_no_section() -> None:
configuration.load_includes() configuration.load_includes()
def test_load_includes_default_path(mocker: MockerFixture) -> None:
"""
must load includes from default path
"""
mocker.patch.object(Configuration, "include", Path("path"))
glob_mock = mocker.patch("pathlib.Path.glob", autospec=True, return_value=[])
Configuration().load_includes()
glob_mock.assert_called_once_with(Path("path"), "*.ini")
def test_merge_sections_missing(configuration: Configuration) -> None: def test_merge_sections_missing(configuration: Configuration) -> None:
""" """
must merge create section if not exists must merge create section if not exists
@@ -134,6 +134,10 @@ def test_validate_path_type(validator: Validator, mocker: MockerFixture) -> None
""" """
error_mock = mocker.patch("ahriman.core.configuration.validator.Validator._error") error_mock = mocker.patch("ahriman.core.configuration.validator.Validator._error")
validator._validate_path_type("file", "field", Path("42"))
mocker.patch("pathlib.Path.exists", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
validator._validate_path_type("file", "field", Path("1")) validator._validate_path_type("file", "field", Path("1"))
+2 -1
View File
@@ -76,4 +76,5 @@ def task_ahriman(package_ahriman: Package, configuration: Configuration, reposit
Returns: Returns:
Task: built task test instance Task: built task test instance
""" """
return Task(package_ahriman, configuration, "x86_64", repository_paths) _, repository_id = configuration.check_loaded()
return Task(package_ahriman, configuration, repository_id, repository_paths)
@@ -2,7 +2,7 @@ import ahriman.web.views
from pathlib import Path from pathlib import Path
from ahriman.core.module_loader import _modules, implementations from ahriman.core.module_loader import _modules, implementations, optional_module
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@@ -23,3 +23,17 @@ def test_implementations() -> None:
assert routes assert routes
assert all(isinstance(view, type) for view in routes) assert all(isinstance(view, type) for view in routes)
assert all(issubclass(view, BaseView) for view in routes) assert all(issubclass(view, BaseView) for view in routes)
def test_optional_module() -> None:
"""
must import an available module
"""
assert optional_module("ahriman.web.views") is ahriman.web.views
def test_optional_module_fallback() -> None:
"""
must return none when the module cannot be imported
"""
assert optional_module("missing_ahriman_module") is None
+11 -15
View File
@@ -17,18 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
try:
import aiohttp_security
except ImportError:
aiohttp_security = None # type: ignore[assignment]
try:
import aiohttp_session
except ImportError:
aiohttp_session = None # type: ignore[assignment]
from typing import Any from typing import Any
from ahriman.core.module_loader import optional_module
__all__ = [ __all__ = [
"authorized_userid", "authorized_userid",
@@ -39,6 +31,10 @@ __all__ = [
] ]
aiohttp_security = optional_module("aiohttp_security")
aiohttp_session = optional_module("aiohttp_session")
async def authorized_userid(*args: Any, **kwargs: Any) -> Any: async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
""" """
handle aiohttp security methods handle aiohttp security methods
@@ -50,7 +46,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
Returns: Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise Any: ``None`` in case if no aiohttp_security module found and function call otherwise
""" """
if aiohttp_security is not None: if aiohttp_security:
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None return None
@@ -66,7 +62,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
Returns: Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise Any: ``None`` in case if no aiohttp_security module found and function call otherwise
""" """
if aiohttp_security is not None: if aiohttp_security:
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None return None
@@ -82,7 +78,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
Returns: Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise Any: ``None`` in case if no aiohttp_security module found and function call otherwise
""" """
if aiohttp_security is not None: if aiohttp_security:
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None return None
@@ -98,7 +94,7 @@ async def get_session(*args: Any, **kwargs: Any) -> Any:
Returns: Returns:
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
""" """
if aiohttp_session is not None: if aiohttp_session:
return await aiohttp_session.get_session(*args, **kwargs) return await aiohttp_session.get_session(*args, **kwargs)
return {} return {}
@@ -114,6 +110,6 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
Returns: Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise Any: ``None`` in case if no aiohttp_security module found and function call otherwise
""" """
if aiohttp_security is not None: if aiohttp_security:
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None return None
@@ -17,15 +17,20 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
try: from ahriman.core.module_loader import optional_module
import aiohttp_apispec # type: ignore[import-untyped]
from marshmallow import Schema, fields
except ImportError: aiohttp_apispec = optional_module("aiohttp_apispec")
marshmallow = optional_module("marshmallow")
if aiohttp_apispec and marshmallow:
Schema = marshmallow.Schema
fields = marshmallow.fields
else:
from unittest.mock import Mock from unittest.mock import Mock
Schema = Mock # type: ignore[misc] Schema = Mock # type: ignore[misc]
aiohttp_apispec = None
fields = Mock() fields = Mock()
@@ -115,7 +115,7 @@ def apidocs(*,
authorization_required = permission != UserAccess.Unauthorized authorization_required = permission != UserAccess.Unauthorized
def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]: def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]:
if aiohttp_apispec is None: if not aiohttp_apispec:
return handler # apispec is disabled return handler # apispec is disabled
responses = _response_schema( responses = _response_schema(
+3 -6
View File
@@ -100,20 +100,17 @@ def _servers(application: Application) -> list[dict[str, Any]]:
}] }]
def setup_apispec(application: Application) -> Any: def setup_apispec(application: Application) -> None:
""" """
setup swagger api specification setup swagger api specification
Args: Args:
application(Application): web application instance application(Application): web application instance
Returns:
Any: created specification instance if module is available
""" """
if aiohttp_apispec is None: if aiohttp_apispec is None:
return None return
return aiohttp_apispec.setup_aiohttp_apispec( aiohttp_apispec.setup_aiohttp_apispec(
application, application,
url="/api-docs/swagger.json", url="/api-docs/swagger.json",
openapi_version="3.0.2", openapi_version="3.0.2",
@@ -17,14 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
try:
import aiohttp_openmetrics
except ImportError:
aiohttp_openmetrics = None # type: ignore[assignment]
from aiohttp.typedefs import Middleware from aiohttp.typedefs import Middleware
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware
from ahriman.core.module_loader import optional_module
from ahriman.web.middlewares import HandlerType from ahriman.web.middlewares import HandlerType
@@ -34,6 +30,9 @@ __all__ = [
] ]
aiohttp_openmetrics = optional_module("aiohttp_openmetrics")
async def metrics(request: Request) -> Response: async def metrics(request: Request) -> Response:
""" """
handler for returning metrics handler for returning metrics
@@ -47,7 +46,7 @@ async def metrics(request: Request) -> Response:
Raises: Raises:
HTTPNotFound: endpoint is disabled HTTPNotFound: endpoint is disabled
""" """
if aiohttp_openmetrics is None: if not aiohttp_openmetrics:
raise HTTPNotFound raise HTTPNotFound
return await aiohttp_openmetrics.metrics(request) return await aiohttp_openmetrics.metrics(request)
@@ -59,7 +58,7 @@ def metrics_handler() -> Middleware:
Returns: Returns:
Middleware: middleware function to handle server metrics Middleware: middleware function to handle server metrics
""" """
if aiohttp_openmetrics is not None: if aiohttp_openmetrics:
return aiohttp_openmetrics.metrics_middleware return aiohttp_openmetrics.metrics_middleware
@middleware @middleware
+1 -1
View File
@@ -60,7 +60,7 @@ async def server_info(view: BaseView) -> dict[str, Any]:
"username": await authorized_userid(view.request), "username": await authorized_userid(view.request),
}, },
"autorefresh_intervals": sorted(autorefresh_intervals, key=comparator), "autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
"docs_enabled": aiohttp_apispec is not None, "docs_enabled": bool(aiohttp_apispec),
"index_url": view.configuration.get("web", "index_url", fallback=None), "index_url": view.configuration.get("web", "index_url", fallback=None),
"repositories": [ "repositories": [
{ {
@@ -50,7 +50,7 @@ class DocsView(BaseView):
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
and returns empty list otherwise and returns empty list otherwise
""" """
if aiohttp_apispec is None: if not aiohttp_apispec:
return [] return []
return cls.ROUTES return cls.ROUTES
@@ -51,7 +51,7 @@ class SwaggerView(BaseView):
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
and returns empty list otherwise and returns empty list otherwise
""" """
if aiohttp_apispec is None: if not aiohttp_apispec:
return [] return []
return cls.ROUTES return cls.ROUTES
@@ -22,6 +22,7 @@ from secrets import token_urlsafe
from typing import ClassVar from typing import ClassVar
from ahriman.core.auth.helpers import get_session, remember from ahriman.core.auth.helpers import get_session, remember
from ahriman.core.module_loader import optional_module
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import LoginSchema, OAuth2Schema from ahriman.web.schemas import LoginSchema, OAuth2Schema
@@ -62,14 +63,12 @@ class LoginView(BaseView):
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
HTTPUnauthorized: if case of authorization error HTTPUnauthorized: if case of authorization error
""" """
try: oauth = optional_module("ahriman.core.auth.oauth")
from ahriman.core.auth.oauth import OAuth if not oauth:
except ImportError:
# no aioauth library found
raise HTTPMethodNotAllowed(self.request.method, ["POST"]) raise HTTPMethodNotAllowed(self.request.method, ["POST"])
oauth_provider = self.validator oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth): if not isinstance(oauth_provider, oauth.OAuth):
raise HTTPMethodNotAllowed(self.request.method, ["POST"]) raise HTTPMethodNotAllowed(self.request.method, ["POST"])
session = await get_session(self.request) session = await get_session(self.request)
@@ -48,7 +48,8 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
must set api specification must set api specification
""" """
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec") apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
assert setup_apispec(application)
setup_apispec(application)
apispec_mock.assert_called_once_with( apispec_mock.assert_called_once_with(
application, application,
url="/api-docs/swagger.json", url="/api-docs/swagger.json",
@@ -37,7 +37,7 @@ async def test_get_import_error(client_with_auth: TestClient, mocker: MockerFixt
""" """
must return 405 on import error must return 405 on import error
""" """
pytest.helpers.import_error("ahriman.core.auth.oauth", ["OAuth"], mocker) mocker.patch("ahriman.web.views.v1.user.login.optional_module", return_value=None)
response = await client_with_auth.get("/api/v1/login") response = await client_with_auth.get("/api/v1/login")
assert response.status == 405 assert response.status == 405
+4 -1
View File
@@ -12,7 +12,8 @@ depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-filelock' 'p
makedepends=('npm' 'python-build' 'python-hatchling' 'python-installer' 'python-wheel') makedepends=('npm' 'python-build' 'python-hatchling' 'python-installer' 'python-wheel')
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$pkgver.tar.gz" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$pkgver.tar.gz"
"$pkgbase.sysusers" "$pkgbase.sysusers"
"$pkgbase.tmpfiles") "$pkgbase.tmpfiles"
"sudoers.conf")
build() { build() {
cd "$pkgbase-$pkgver" cd "$pkgbase-$pkgver"
@@ -59,6 +60,8 @@ package_ahriman-core() {
install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf" install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf"
install -Dm644 "$srcdir/$pkgbase.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgbase.conf" install -Dm644 "$srcdir/$pkgbase.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgbase.conf"
install -Dm440 "$srcdir/sudoers.conf" "$pkgdir/etc/sudoers.d/$pkgname"
} }
package_ahriman-triggers() { package_ahriman-triggers() {
+1
View File
@@ -0,0 +1 @@
ahriman ALL= NOPASSWD:SETENV: /usr/bin/ahriman-archbuild *
+1 -1
View File
@@ -48,7 +48,7 @@ if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
fi fi
[ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND" [ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND"
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}" sudo -E -u "$AHRIMAN_USER" -- ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
[ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND" [ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND"
# validate configuration if set # validate configuration if set
+5 -3
View File
@@ -82,7 +82,7 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually. * ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
* ``database`` - path to the application SQLite database, string, required. * ``database`` - path to the application SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order. * ``include`` - path to directories with configuration files overrides, space separated list of strings, optional. Files will be read in alphabetical order.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups ``alpm:*`` groups
@@ -131,12 +131,14 @@ Authorized users are stored inside internal database, if any of external provide
Build related configuration. Group name can refer to architecture, e.g. ``build:x86_64`` can be used for x86_64 architecture specific settings. Build related configuration. Group name can refer to architecture, e.g. ``build:x86_64`` can be used for x86_64 architecture specific settings.
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional. * ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
* ``build_command`` - default build command, string, required. * ``devtools_wrapper`` - path to devtools wrapper, space separated list of strings, required.
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional. * ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``. * ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional. * ``make_flags`` - additional flags passed to ``make`` command via environment variable, space separated list of strings, optional.
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional. * ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
* ``min_age`` - minimal age in seconds since the latest AUR package modification before automatic updates are allowed, integer, optional, default ``0``. * ``min_age`` - minimal age in seconds since the latest AUR package modification before automatic updates are allowed, integer, optional, default ``0``.
* ``packager`` - default packager identifier in form ``Name Surname <mail@example.com>``, string, optional.
* ``scan_paths`` - paths to be used for implicit dependencies scan, space separated list of strings, optional. If any of those paths is matched against the path, it will be added to the allowed list. * ``scan_paths`` - paths to be used for implicit dependencies scan, space separated list of strings, optional. If any of those paths is matched against the path, it will be added to the allowed list.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition. * ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation. * ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
+2 -26
View File
@@ -28,18 +28,11 @@ Initial setup
Configure build tools (it is required for correct dependency management system): Configure build tools (it is required for correct dependency management system):
#. #.
Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``): Create configuration file ``{name}.conf`` or ``{name}-{arch}.conf``, where ``name`` is the repostory name and ``arch`` is the repository architecture, e.g.:
.. code-block:: shell .. code-block:: shell
ln -s /usr/bin/archbuild /usr/local/bin/aur-x86_64-build cp /usr/share/devtools/pacman.conf.d/{extra,aur-x86_64}.conf
#.
Create configuration file (same as previous ``{name}.conf``):
.. code-block:: shell
cp /usr/share/devtools/pacman.conf.d/{extra,aur}.conf
#. #.
Change configuration file, add your own repository, add multilib repository etc: Change configuration file, add your own repository, add multilib repository etc:
@@ -53,23 +46,6 @@ Initial setup
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman.conf.d/aur-x86_64.conf echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman.conf.d/aur-x86_64.conf
echo 'Server = file:///var/lib/ahriman/repository/$repo/$arch' | tee -a /usr/share/devtools/pacman.conf.d/aur-x86_64.conf echo 'Server = file:///var/lib/ahriman/repository/$repo/$arch' | tee -a /usr/share/devtools/pacman.conf.d/aur-x86_64.conf
#.
Set ``build_command`` option to point to your command:
.. code-block:: shell
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
echo 'build_command = aur-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
#.
Configure ``/etc/sudoers.d/ahriman`` to allow running command without a password:
.. code-block:: shell
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/aur-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
This command supports several arguments, kindly refer to its help message. This command supports several arguments, kindly refer to its help message.
#. #.
+2 -1
View File
@@ -23,7 +23,8 @@ allow_read_only = no
[build] [build]
archbuild_flags = archbuild_flags =
build_command = extra-x86_64-build devtools_configs = .
devtools_wrapper = ahriman-archbuild
ignore_packages = ignore_packages =
makechrootpkg_flags = makechrootpkg_flags =
makepkg_flags = --skippgpcheck makepkg_flags = --skippgpcheck
+21 -1
View File
@@ -93,6 +93,21 @@ class ImportOrder(BaseRawFileChecker):
) )
) )
@staticmethod
def import_context(statement: nodes.Import | nodes.ImportFrom) -> tuple[nodes.NodeNG, str]:
"""
extract the lexical block containing an import
Args:
statement(nodes.Import | nodes.ImportFrom): import node
Returns:
tuple[nodes.NodeNG, str]: parent node and its branch containing the import
"""
parent = statement.parent
field, _ = parent.locate_child(statement)
return parent, field
@staticmethod @staticmethod
def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]: def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]:
""" """
@@ -192,7 +207,12 @@ class ImportOrder(BaseRawFileChecker):
node(nodes.Module): module node to check node(nodes.Module): module node to check
""" """
root_module, *_ = node.qname().split(".") root_module, *_ = node.qname().split(".")
self.check_imports(self.imports(node.values()), root_module) contexts: dict[tuple[nodes.NodeNG, str], list[nodes.Import | nodes.ImportFrom]] = {}
for statement in self.imports(node.values()):
contexts.setdefault(self.import_context(statement), []).append(statement)
for imports in contexts.values():
self.check_imports(imports, root_module)
def register(linter: PyLinter) -> None: def register(linter: PyLinter) -> None:
@@ -117,36 +117,12 @@ def get_package_status_extended(package: Package) -> dict[str, Any]:
return {"status": BuildStatus().view(), "package": package.view()} return {"status": BuildStatus().view(), "package": package.view()}
def import_error(package: str, components: list[str], mocker: MockerFixture) -> MagicMock:
"""
mock import error
Args:
package(str): package name to import
components(list[str]): component to import if any (e.g. from ... import ...)
mocker(MockerFixture): mocker object
Returns:
MagicMock: mocked object
"""
import builtins
_import = builtins.__import__
# pylint: disable=redefined-builtin
def test_import(name: str, globals: Any, locals: Any, from_list: list[str], level: Any):
if name == package and (not components or any(component in from_list for component in components)):
raise ImportError
return _import(name, globals, locals, from_list, level)
return mocker.patch.object(builtins, "__import__", test_import)
@pytest.hookimpl(trylast=True) @pytest.hookimpl(trylast=True)
def pytest_configure() -> None: def pytest_configure() -> None:
""" """
register helpers after pytest-helpers-namespace has initialized register helpers after pytest-helpers-namespace has initialized
""" """
for helper in (anyvar, get_package_status, get_package_status_extended, import_error): for helper in (anyvar, get_package_status, get_package_status_extended):
pytest.helpers.register(helper) pytest.helpers.register(helper)
+14 -11
View File
@@ -2,7 +2,6 @@ env_list = [
"check", "check",
"tests", "tests",
] ]
isolated_build = true
requires = [ requires = [
"tox-uv", "tox-uv",
] ]
@@ -33,6 +32,17 @@ bandit = [
"--configfile", "--configfile",
".bandit.toml", ".bandit.toml",
] ]
coverage_report = [
"--show-missing",
"--skip-covered",
"--fail-under=100",
]
coverage_run = [
"--source",
"ahriman",
"-m",
"pytest",
]
manpage = [ manpage = [
"--author", "--author",
"{[project]name} team", "{[project]name} team",
@@ -110,6 +120,7 @@ dependency_groups = [
] ]
deps = [ deps = [
{ replace = "ref", of = ["project", "extras"], extend = true }, { replace = "ref", of = ["project", "extras"], extend = true },
"pip",
] ]
pip_pre = true pip_pre = true
set_env.CFLAGS = "-Wno-unterminated-string-initialization" set_env.CFLAGS = "-Wno-unterminated-string-initialization"
@@ -274,15 +285,12 @@ deps = [
{ replace = "ref", of = ["project", "extras"], extend = true }, { replace = "ref", of = ["project", "extras"], extend = true },
] ]
pip_pre = true pip_pre = true
recreate = true
commands = [ commands = [
[ [
"sphinx-build", "sphinx-build",
"--builder", "--builder",
"html", "html",
"--fail-on-warning", "--fail-on-warning",
"--jobs",
"1",
"--write-all", "--write-all",
"docs", "docs",
"{envtmpdir}/html", "{envtmpdir}/html",
@@ -352,10 +360,7 @@ commands = [
[ [
"coverage", "coverage",
"run", "run",
"--source", { replace = "ref", of = ["flags", "coverage_run"], extend = true },
"ahriman",
"-m",
"pytest",
{ replace = "posargs", default = [ { replace = "posargs", default = [
"tests/test_tests.py", "tests/test_tests.py",
"ahriman-core/tests", "ahriman-core/tests",
@@ -366,9 +371,7 @@ commands = [
[ [
"coverage", "coverage",
"report", "report",
"--show-missing", { replace = "ref", of = ["flags", "coverage_report"], extend = true },
"--skip-covered",
"--fail-under=100",
], ],
] ]