Compare commits

..
8 Commits
Author SHA1 Message Date
arcanis 0094268eed use local devtools configs instead of global ones 2026-08-10 16:25:38 +03:00
arcanis 8f70cb3d8d 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-10 14:24:13 +03:00
arcanis 9780b9821b feat: read includes from list of directories 2026-08-10 01:37:24 +03:00
arcanis e182e110d4 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-09 17:37:38 +03:00
arcanis 9c3d1471e0 build: tox cleanup 2026-07-23 15:54:26 +03:00
arcanis f37c5bd2a2 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-07-22 16:33:59 +03:00
arcanis bd26e8da68 build: handle local imports in pylint check correctly 2026-07-22 16:18:56 +03:00
arcanis 99365ea78f build: fix mypy type install 2026-07-22 15:54:00 +03:00
8 changed files with 69 additions and 139 deletions
@@ -28,7 +28,7 @@ from urllib.parse import quote_plus as url_encode
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeError, MissingArchitectureError
from ahriman.core.exceptions import MissingArchitectureError
from ahriman.core.utils import enum_values
from ahriman.models.repository_id import RepositoryId
from ahriman.models.sign_settings import SignSettings
@@ -63,20 +63,24 @@ class Setup(Handler):
if args.architecture is None or args.repository is None:
raise MissingArchitectureError(args.command)
target_directory = Setup.configuration_create_directory(configuration)
Setup.configuration_create_ahriman(args, repository_id, configuration, target_directory)
Setup.configuration_create_ahriman(args, repository_id, configuration)
configuration.reload()
application = Application(repository_id, configuration, report=report)
paths = application.repository.paths
repository_server = f"file://{paths.repository}" if args.server is None else args.server
target_directory = paths.ensure_exists(configuration.getpath("build", "devtools_configs"))
Setup.configuration_create_devtools(repository_id, args.from_configuration, target_directory, args.mirror,
args.multilib, repository_server)
# basically we create configuration here as root, but it is ok, because those files are only used for reading
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id,
args.from_configuration,
configuration.repository_paths.ensure_exists(configuration.getpath("build", "devtools_configs")),
args.mirror,
args.multilib,
repository_server,
)
# finish initialization
with paths.preserve_owner():
with application.repository.paths.preserve_owner():
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
@@ -121,7 +125,7 @@ class Setup(Handler):
@staticmethod
def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId,
root: Configuration, target_directory: Path) -> None:
root: Configuration) -> None:
"""
create service specific configuration
@@ -129,7 +133,6 @@ class Setup(Handler):
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
root(Configuration): root configuration instance
target_directory(Path): path to directory where configuration files will be written
"""
configuration = Configuration()
@@ -167,7 +170,9 @@ class Setup(Handler):
if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20))
target = target_directory / f"00-setup-overrides-{repository_id.id}.ini"
include_path = next(path for path in root.include if os.access(path, os.W_OK))
(include_path / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = include_path / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w", encoding="utf8") as ahriman_configuration:
configuration.write(ahriman_configuration)
@@ -222,28 +227,4 @@ class Setup(Handler):
with target.open("w", encoding="utf8") as devtools_configuration:
configuration.write(devtools_configuration)
@staticmethod
def configuration_create_directory(root: Configuration) -> Path:
"""
create directory for includes
Args:
root(Configuration): root configuration 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
raise InitializeError("No writable include directory found")
arguments = [_set_service_setup_parser]
@@ -119,9 +119,12 @@ class Task(LazyLogging):
command = self._legacy_build_command[:]
if not command:
command = self.build_command + [
"-r", self.repository_id.name,
"-a", self.repository_id.architecture,
"-c", str(self.devtools_configs),
"-r",
self.repository_id.name,
"-a",
self.repository_id.architecture,
"-c",
str(self.devtools_configs),
"--",
]
@@ -108,6 +108,16 @@ class Configuration(configparser.RawConfigParser):
_, repository_id = self.check_loaded()
return repository_id.architecture
@property
def include(self) -> list[Path]:
"""
get full path to include directory(ies)
Returns:
list[Path]: path to directory with configuration includes
"""
return self.getpathlist("settings", "include")
@property
def logging_path(self) -> Path:
"""
@@ -322,28 +332,14 @@ class Configuration(configparser.RawConfigParser):
self.includes = [] # reset state
try:
# 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 path in self.include: # pylint: disable=not-an-iterable
for include in sorted(path.glob("*.ini")):
if include == self.logging_path:
continue # we don't want to load logging explicitly
self.read(include)
self.includes.append(include)
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass
def merge_sections(self, repository_id: RepositoryId) -> None:
"""
@@ -403,7 +399,6 @@ class Configuration(configparser.RawConfigParser):
# create another instance and copy values from there
instance = self.from_path(path, repository_id)
self.copy_from(instance)
self.includes = instance.includes
def set_option(self, section: str, option: str, value: str) -> None:
"""
@@ -1,6 +1,5 @@
import argparse
import multiprocessing
import os
import pytest
from pathlib import Path
@@ -12,8 +11,9 @@ from urllib.parse import quote_plus as url_encode
from ahriman.application.handlers.setup import Setup
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import InitializeError, MissingArchitectureError
from ahriman.core.exceptions import MissingArchitectureError
from ahriman.core.repository import Repository
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.sign_settings import SignSettings
@@ -51,11 +51,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
must run command
"""
args = _default_args(args)
local = Path("local")
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
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")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
@@ -64,16 +61,9 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
_, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False)
owner_guard_mock.assert_called_once_with()
mkdir_mock.assert_called_once_with(configuration)
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration, local)
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration)
devtools_configuration_mock.assert_called_once_with(
repository_id,
args.from_configuration,
configuration.getpath("build", "devtools_configs"),
args.mirror,
args.multilib,
f"file://{repository_paths.repository}",
)
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
init_mock.assert_called_once_with()
@@ -112,17 +102,11 @@ def test_run_with_server(args: argparse.Namespace, configuration: Configuration,
_, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False)
devtools_configuration_mock.assert_called_once_with(
repository_id,
args.from_configuration,
configuration.getpath("build", "devtools_configs"),
args.mirror,
args.multilib,
"server",
)
repository_id, args.from_configuration, args.mirror, args.multilib, "server")
def test_configuration_create_ahriman(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
mocker: MockerFixture) -> None:
def test_configuration_create_ahriman(args: argparse.Namespace, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must create configuration for the service
"""
@@ -130,9 +114,10 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
mocker.patch("pathlib.Path.open")
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
remove_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
_, repository_id = configuration.check_loaded()
Setup.configuration_create_ahriman(args, repository_id, configuration, tmp_path)
Setup.configuration_create_ahriman(args, repository_id, configuration)
set_option_mock.assert_has_calls([
MockCall("repository", "name", repository_id.name),
MockCall(Configuration.section_name("build", repository_id.name, repository_id.architecture),
@@ -154,10 +139,15 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
remove_mock.assert_called_once_with(
next(
path for path in configuration.include) /
"00-setup-overrides.ini",
missing_ok=True)
def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, configuration: Configuration,
tmp_path: Path, mocker: MockerFixture) -> None:
mocker: MockerFixture) -> None:
"""
must create configuration for the service without multilib repository
"""
@@ -168,14 +158,14 @@ def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, conf
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
_, repository_id = configuration.check_loaded()
Setup.configuration_create_ahriman(args, repository_id, configuration, tmp_path)
Setup.configuration_create_ahriman(args, repository_id, configuration)
set_option_mock.assert_has_calls([
MockCall(Configuration.section_name("alpm", repository_id.name, repository_id.architecture), "mirror",
args.mirror),
]) # non-strict check called intentionally
def test_configuration_create_devtools(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
def test_configuration_create_devtools(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools
@@ -187,12 +177,12 @@ def test_configuration_create_devtools(args: argparse.Namespace, configuration:
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
_, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(repository_id, args.from_configuration, tmp_path, None, args.multilib, "server")
Setup.configuration_create_devtools(repository_id, args.from_configuration, None, args.multilib, "server")
add_section_mock.assert_has_calls([MockCall("multilib"), MockCall(repository_id.name)])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_configuration_create_devtools_mirror(args: argparse.Namespace, configuration: Configuration, tmp_path: Path,
def test_configuration_create_devtools_mirror(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools with mirror set explicitly
@@ -212,21 +202,14 @@ def test_configuration_create_devtools_mirror(args: argparse.Namespace, configur
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
_, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(
repository_id,
args.from_configuration,
tmp_path,
args.mirror,
args.multilib,
"server",
)
Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, args.multilib, "server")
get_mock.assert_has_calls([MockCall("core", "Include", fallback=None), MockCall("extra", "Include", fallback=None)])
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
def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, configuration: Configuration,
tmp_path: Path, mocker: MockerFixture) -> None:
mocker: MockerFixture) -> None:
"""
must create configuration for the devtools without multilib
"""
@@ -236,33 +219,10 @@ def test_configuration_create_devtools_no_multilib(args: argparse.Namespace, con
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
_, repository_id = configuration.check_loaded()
Setup.configuration_create_devtools(repository_id, args.from_configuration, tmp_path, args.mirror, False, "server")
Setup.configuration_create_devtools(repository_id, args.from_configuration, args.mirror, False, "server")
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_configuration_create_directory(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must create writable directory for includes
"""
configuration.set_option("settings", "include", f"/path1 /path2 /path3")
mkdir_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.ensure_exists",
side_effect=[OSError, "/path2", "/path3"])
access_mock = mocker.patch("os.access", side_effect=[False, True])
assert Setup.configuration_create_directory(configuration) == "/path3"
mkdir_mock.assert_has_calls([MockCall(Path("/path1")), MockCall(Path("/path2")), MockCall(Path("/path3"))])
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_directory_no_writable(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must raise InitializeError if no writable directories found
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.ensure_exists", side_effect=OSError)
with pytest.raises(InitializeError):
Setup.configuration_create_directory(configuration)
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run
@@ -63,7 +63,7 @@ def test_run_default(args: argparse.Namespace, configuration: Configuration) ->
default = Configuration.from_path(Configuration.SYSTEM_CONFIGURATION_PATH, repository_id)
# copy autogenerated values
for section, key in (("repository", "root"), ("build", "devtools_configs")):
for section, key in (("repository", "root"),):
value = configuration.get(section, key)
default.set_option(section, key, value)
@@ -54,9 +54,7 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
assert task_ahriman.build(local) == [task_ahriman.package.base]
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", task_ahriman.repository_id.name, "-a", task_ahriman.repository_id.architecture,
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
@@ -83,9 +81,7 @@ def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
task_ahriman.build(local, **environment, empty=None)
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", task_ahriman.repository_id.name, "-a", task_ahriman.repository_id.architecture,
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
@@ -110,9 +106,7 @@ def test_build_makeflags(task_ahriman: Task, mocker: MockerFixture) -> None:
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", task_ahriman.repository_id.name, "-a", task_ahriman.repository_id.architecture,
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
@@ -136,9 +130,7 @@ def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None:
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", task_ahriman.repository_id.name, "-a", task_ahriman.repository_id.architecture,
"--", "-r", str(task_ahriman.paths.chroot),
"--", "-D", str(task_ahriman.paths.archive),
"--", "--skippgpcheck",
+1 -1
View File
@@ -48,7 +48,7 @@ if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
fi
[ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND"
sudo -E -u "$AHRIMAN_USER" -- ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
[ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND"
# validate configuration if set
-1
View File
@@ -120,7 +120,6 @@ dependency_groups = [
]
deps = [
{ replace = "ref", of = ["project", "extras"], extend = true },
"pip",
]
pip_pre = true
set_env.CFLAGS = "-Wno-unterminated-string-initialization"