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
This commit is contained in:
2026-08-09 17:37:38 +03:00
parent 9c3d1471e0
commit e182e110d4
18 changed files with 123 additions and 160 deletions
+33
View File
@@ -0,0 +1,33 @@
#!/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"
exit 1
}
repository=
architecture=
while getopts ":r:a:" arg; do
case "$arg" in
r) repository="$OPTARG" ;;
a) architecture="$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]}" "$@"
exec bash -c '
source "$1" "${@:2}"
' "${repository}-${architecture}-build" "archbuild" "${@:$OPTIND}"
@@ -35,7 +35,7 @@ retry_backoff = 1.0
; List of additional flags passed to archbuild command.
;archbuild_flags =
; Path to build command.
;build_command =
devtools_wrapper = ahriman-archbuild
; List of packages to be ignored during automatic updates.
;ignore_packages =
; Include debug packages.
+1
View File
@@ -57,6 +57,7 @@ packages = [
]
[tool.hatch.build.targets.wheel.shared-data]
"package/bin" = "bin"
"package/lib" = "lib"
"package/share" = "share"
@@ -40,16 +40,12 @@ class Setup(Handler):
setup handler
Attributes:
ARCHBUILD_COMMAND_PATH(Path): (class attribute) default devtools command
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
ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
@@ -74,11 +70,9 @@ class Setup(Handler):
# basically we create configuration here as root, but it is ok, because those files are only used for reading
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
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, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
# finish initialization
with application.repository.paths.preserve_owner():
@@ -124,20 +118,6 @@ class Setup(Handler):
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
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
def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId,
root: Configuration) -> None:
@@ -152,8 +132,6 @@ class Setup(Handler):
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
if args.build_as_user is not None:
configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}")
@@ -257,31 +235,4 @@ class Setup(Handler):
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]
@@ -56,7 +56,7 @@ class PackageVersion(LazyLogging):
_, repository_id = configuration.check_loaded()
paths = configuration.repository_paths
task = Task(self.package, configuration, repository_id.architecture, paths)
task = Task(self.package, configuration, repository_id, paths)
try:
# 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.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
@@ -36,32 +37,33 @@ class Task(LazyLogging):
Attributes:
archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture
build_command(str): build command
build_command(list[str]): build command
include_debug_packages(bool): whether to include debug packages or not
makechrootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
repository_id(RepositoryId): repository unique identifier
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:
"""
Args:
package(Package): package definitions
configuration(Configuration): configuration instance
architecture(str): repository architecture
repository_id(RepositoryId): repository unique identifier
paths(RepositoryPaths): repository paths instance
"""
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
self.architecture = architecture
self.repository_id = repository_id
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._legacy_build_command = configuration.getlist("build", "build_command", fallback=[])
self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True)
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
@@ -108,12 +110,16 @@ class Task(LazyLogging):
Returns:
list[Path]: paths of produced packages
"""
command = [self.build_command, "-r", str(self.paths.chroot)]
command.extend(self.archbuild_flags)
command.extend(["--", "-D", str(self.paths.archive)] + self.makechrootpkg_flags)
command.extend(["--"] + self.makepkg_flags)
command = self._legacy_build_command[:]
if not command:
command = self.build_command + ["-r", self.repository_id.name, "-a", self.repository_id.architecture, "--"]
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:
command.extend(["--nobuild"])
self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = {
@@ -156,7 +162,7 @@ class Task(LazyLogging):
return last_commit_sha
# 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:
self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
patch = PkgbuildPatch("pkgrel", pkgrel)
@@ -190,8 +190,20 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
},
"build_command": {
"type": "string",
"required": True,
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"devtools_wrapper": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
"empty": False,
},
"ignore_packages": {
@@ -73,7 +73,7 @@ class Executor(PackageInfo, Cleaner):
"""
self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.repository_id.architecture, self.paths)
task = Task(package, self.configuration, self.repository_id, self.paths)
patches = self.reporter.package_patches_get(package.base, None)
commit_sha = task.init(path, patches, local_version)
@@ -55,8 +55,6 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
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")
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")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
@@ -67,8 +65,6 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
devtools_configuration_mock.assert_called_once_with(
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
makepkg_configuration_mock.assert_called_once_with(args.packager, args.makeflags_jobs, repository_paths)
sudo_configuration_mock.assert_called_once_with(repository_paths, repository_id)
executable_mock.assert_called_once_with(repository_paths, repository_id)
init_mock.assert_called_once_with()
@@ -102,8 +98,6 @@ def test_run_with_server(args: argparse.Namespace, configuration: Configuration,
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_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")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools")
@@ -113,17 +107,6 @@ def test_run_with_server(args: argparse.Namespace, configuration: Configuration,
repository_id, args.from_configuration, args.mirror, args.multilib, "server")
def test_build_command(repository_id: RepositoryId) -> 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:
"""
@@ -135,12 +118,9 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
remove_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
_, repository_id = configuration.check_loaded()
command = Setup.build_command(repository_paths.root, repository_id)
Setup.configuration_create_ahriman(args, repository_id, configuration)
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(Configuration.section_name("build", repository_id.name, repository_id.architecture),
"makechrootpkg_flags", f"-U {args.build_as_user}"),
@@ -251,34 +231,6 @@ def test_configuration_create_makepkg(args: argparse.Namespace, repository_paths
Path("home") / ".makepkg.conf", pytest.helpers.anyvar(str, True), encoding="utf8")
def test_configuration_create_sudo(configuration: Configuration, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must create sudo configuration
"""
chmod_text_mock = mocker.patch("pathlib.Path.chmod")
write_text_mock = mocker.patch("pathlib.Path.write_text")
_, 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:
"""
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 (("build", "build_command"), ("repository", "root")):
for section, key in (("repository", "root"),):
value = configuration.get(section, key)
default.set_option(section, key, value)
@@ -53,8 +53,9 @@ 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(
"extra-x86_64-build",
"-r", str(task_ahriman.paths.chroot),
"ahriman-archbuild",
"-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",
exception=pytest.helpers.anyvar(int),
@@ -79,8 +80,9 @@ def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
task_ahriman.build(local, **environment, empty=None)
check_output_mock.assert_called_once_with(
"extra-x86_64-build",
"-r", str(task_ahriman.paths.chroot),
"ahriman-archbuild",
"-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",
exception=pytest.helpers.anyvar(int),
@@ -101,12 +103,37 @@ def test_build_dry_run(task_ahriman: Task, mocker: MockerFixture) -> None:
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
assert task_ahriman.build(local, dry_run=True) == [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,
"--", "-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(
"extra-x86_64-build",
"-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,
+2 -1
View File
@@ -76,4 +76,5 @@ def task_ahriman(package_ahriman: Package, configuration: Configuration, reposit
Returns:
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)