Skip to content

Commit

Permalink
Add rpmautospec plugin
Browse files Browse the repository at this point in the history
This plugin preprocesses spec files using %autorelease and/or
%autochangelog in the mock chroot prior to building SRPMs. The
`rpmautospec` package will be installed into the chroot if necessary.

Co-authored-by: Nils Philippsen <[email protected]>
Signed-off-by: Stephen Gallagher <[email protected]>
Signed-off-by: Nils Philippsen <[email protected]>
  • Loading branch information
sgallagher and nphilipp committed Nov 17, 2023
1 parent ba2a10b commit 6986b10
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 2 deletions.
11 changes: 10 additions & 1 deletion mock/py/mockbuild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
PLUGIN_LIST = ['tmpfs', 'root_cache', 'yum_cache', 'mount', 'bind_mount',
'ccache', 'selinux', 'package_state', 'chroot_scan',
'lvm_root', 'compress_logs', 'sign', 'pm_request',
'hw_info', 'procenv', 'showrc', 'rpkg_preprocessor']
'hw_info', 'procenv', 'showrc', 'rpkg_preprocessor',
'rpmautospec']

def nspawn_supported():
"""Detect some situations where the systemd-nspawn chroot code won't work"""
Expand Down Expand Up @@ -202,6 +203,14 @@ def setup_default_config_opts():
'requires': ['preproc-rpmspec'],
'cmd': '/usr/bin/preproc-rpmspec %(source_spec)s --output %(target_spec)s',
},
'rpmautospec_enable': False,
'rpmautospec_opts': {
'requires': ['rpmautospec'],
'cmd_base': [
'/usr/bin/rpmautospec',
'process-distgit',
]
},
}

config_opts['environment'] = {
Expand Down
Empty file.
117 changes: 117 additions & 0 deletions mock/py/mockbuild/plugins/rpmautospec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:textwidth=0:
# License: GPL2 or later see COPYING
# Copyright (C) 2023 Stephen Gallagher <[email protected]>
# Copyright (C) 2023 Nils Philippsen <[email protected]>
"""A mock plugin to pre-process spec files using rpmautospec."""

from pathlib import Path
from typing import Optional, Union

from rpmautospec_core import specfile_uses_rpmautospec

from mockbuild.exception import ConfigError
from mockbuild.trace_decorator import getLog, traceLog

requires_api_version = "1.1"


@traceLog()
def init(plugins, conf, buildroot):
"""Register the rpmautospec plugin with mock."""
RpmautospecPlugin(plugins, conf, buildroot)


class RpmautospecPlugin:
"""Fill in release and changelog from git history using rpmautospec"""

@traceLog()
def __init__(self, plugins, conf, buildroot):
self.buildroot = buildroot
self.config = buildroot.config
self.opts = conf
self.log = getLog()

if "cmd_base" not in self.opts:
raise ConfigError("The 'rpmautospec_opts.cmd_base' is unset")

plugins.add_hook("pre_srpm_build", self.attempt_process_distgit)
self.log.info("rpmautospec: initialized")

@traceLog()
def attempt_process_distgit(
self,
host_chroot_spec: Union[Path, str],
host_chroot_sources: Optional[Union[Path, str]],
) -> None:
"""Attempt to process a spec file with rpmautospec."""
# Set up variables and check prerequisites.
if not host_chroot_sources:
self.log.debug("Sources not specified, skipping rpmautospec preprocessing.")
return

host_chroot_spec = Path(host_chroot_spec)
host_chroot_sources = Path(host_chroot_sources)
if not host_chroot_sources.is_dir():
self.log.debug(
"Sources not a directory, skipping rpmautospec preprocessing."
)
return

distgit_git_dir = host_chroot_sources / ".git"
if not distgit_git_dir.is_dir():
self.log.debug(
"Sources is not a git repository, skipping rpmautospec preprocessing."
)
return

host_chroot_sources_spec = host_chroot_sources / host_chroot_spec.name
if not host_chroot_sources_spec.is_file():
self.log.debug(
"Sources doesn’t contain spec file, skipping rpmautospec preprocessing."
)
return

with host_chroot_spec.open("rb") as spec, host_chroot_sources_spec.open(
"rb"
) as sources_spec:
if spec.read() != sources_spec.read():
self.log.warning(
"Spec file inside and outside sources are different, skipping rpmautospec"
" preprocessing."
)
return

if not specfile_uses_rpmautospec(host_chroot_sources_spec):
self.log.debug(
"Spec file doesn’t use rpmautospec, skipping rpmautospec preprocessing."
)
return

# Install the `rpmautospec` command line tool into the build root.
if self.opts.get("requires", None):
self.buildroot.pkg_manager.install_as_root(*self.opts["requires"], check=True)

# Get paths inside the chroot by chopping off the leading paths
chroot_dir = Path(self.buildroot.make_chroot_path())
chroot_spec = Path("/") / host_chroot_spec.relative_to(chroot_dir)
chroot_sources = Path("/") / host_chroot_sources.relative_to(chroot_dir)
chroot_sources_spec = Path("/") / host_chroot_sources_spec.relative_to(chroot_dir)

# Call subprocess to perform the specfile rewrite
command = list(self.opts["cmd_base"])
command += [chroot_sources_spec] # <input-spec>
command += [chroot_spec] # <output-spec>

self.buildroot.doChroot(
command,
shell=False,
cwd=chroot_sources,
logger=self.buildroot.build_log,
uid=self.buildroot.chrootuid,
gid=self.buildroot.chrootgid,
user=self.buildroot.chrootuser,
unshare_net=not self.config.get("rpmbuild_networking", False),
nspawn_args=self.config.get("nspawn_args", []),
printOutput=self.config.get("print_main_output", True),
)
1 change: 1 addition & 0 deletions mock/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ distro
jinja2
pyroute2
requests
rpmautospec-core
templated-dictionary
Empty file added mock/tests/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions mock/tests/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Compatibility code for old Python versions:"""

try:
from contextlib import nullcontext # noqa: F401
except ImportError:
# Python < 3.7
from contextlib import contextmanager

@contextmanager
def nullcontext(enter_result=None):
"""Trimmed down version of contextlib.nullcontext()"""
yield enter_result
Empty file added mock/tests/plugins/__init__.py
Empty file.
185 changes: 185 additions & 0 deletions mock/tests/plugins/test_rpmautospec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Test the rpmautospec plugin."""

from copy import deepcopy
from pathlib import Path
from unittest import mock

import pytest

from mockbuild.exception import ConfigError
from mockbuild.plugins import rpmautospec

from ..compat import nullcontext

UNSET = object()


@mock.patch("mockbuild.plugins.rpmautospec.RpmautospecPlugin")
def test_init(RpmautospecPlugin): # noqa: C0103
"""Test the function which registers the plugin."""
plugins = object()
conf = object()
buildroot = object()

rpmautospec.init(plugins, conf, buildroot)

RpmautospecPlugin.assert_called_once_with(plugins, conf, buildroot)


class TestRpmautospecPlugin:
"""Test the RpmautospecPlugin class."""

DEFAULT_OPTS = {
"requires": ["rpmautospec"],
"cmd_base": ["rpmautospec", "process-distgit"],
}

def create_plugin(self, plugins=UNSET, conf=UNSET, buildroot=UNSET):
"""Create a plugin object and prepare it for testing."""
if plugins is UNSET:
plugins = mock.Mock()
if conf is UNSET:
conf = deepcopy(self.DEFAULT_OPTS)
if buildroot is UNSET:
buildroot = mock.Mock()

return rpmautospec.RpmautospecPlugin(plugins, conf, buildroot)

@pytest.mark.parametrize(
"with_cmd_base", (True, False), ids=("with-cmd_base", "without-cmd_base")
)
@mock.patch("mockbuild.plugins.rpmautospec.getLog")
def test___init__(self, getLog, with_cmd_base):
"""Test the constructor."""
plugins = mock.Mock()

conf = deepcopy(self.DEFAULT_OPTS)
if with_cmd_base:
expectation = nullcontext()
else:
expectation = pytest.raises(ConfigError)
del conf["cmd_base"]

buildroot = mock.Mock()

with expectation as exc_info:
logger = getLog.return_value
plugin = self.create_plugin(plugins=plugins, conf=conf, buildroot=buildroot)

if with_cmd_base:
assert plugin.buildroot is buildroot
assert plugin.config is buildroot.config
assert plugin.opts is conf
assert plugin.log is logger
plugins.add_hook.assert_called_once_with("pre_srpm_build", plugin.attempt_process_distgit)
logger.info.assert_called_once_with("rpmautospec: initialized")
else:
assert "rpmautospec_opts.cmd_base" in str(exc_info.value)

@pytest.mark.parametrize(
"testcase",
(
"happy-path",
"happy-path-no-requires",
"without-sources",
"sources-not-dir",
"sources-not-repo",
"sources-no-specfile",
"spec-files-different",
"specfile-no-rpmautospec",
),
)
def test_attempt_process_distgit(self, testcase, tmp_path): # noqa: R0912, R0915
"""Test the attempt_process_distgit() method."""
# Set the stage
plugin = self.create_plugin()
plugin.log = log = mock.Mock()
plugin.buildroot.make_chroot_path.return_value = str(tmp_path)
if "no-requires" in testcase:
plugin.opts["requires"] = []

spec_dir = tmp_path / "SPECS"
spec_dir.mkdir()
sources_dir = tmp_path / "SOURCES"
sources_dir.mkdir()

host_chroot_spec = spec_dir / "pkg.spec"
host_chroot_sources = sources_dir / "pkg"
host_chroot_sources.mkdir()
host_chroot_sources_git = host_chroot_sources / ".git"
host_chroot_sources_spec = host_chroot_sources / "pkg.spec"

if "no-rpmautospec" not in testcase:
spec_contents = (
"Release: %autorelease",
"%changelog",
"%autochangelog",
)
else:
spec_contents = (
"Release: 1",
"%changelog",
)

with host_chroot_spec.open("w") as fp:
for line in spec_contents:
print(line, file=fp)

if "without-sources" in testcase:
host_chroot_sources = None
elif "sources-not-dir" not in testcase:
if "sources-no-specfile" not in testcase:
with host_chroot_sources_spec.open("w") as fp:
if "spec-files-different" in testcase:
print("# BOO", file=fp)
for line in spec_contents:
print(line, file=fp)
if "sources-not-repo" not in testcase:
host_chroot_sources_git.mkdir()
else:
host_chroot_sources = tmp_path / "pkg.tar"
host_chroot_sources.touch()

plugin.attempt_process_distgit(host_chroot_spec, host_chroot_sources)

if "happy-path" in testcase:
chroot_spec = Path("/") / host_chroot_spec.relative_to(tmp_path)
chroot_sources = Path("/") / host_chroot_sources.relative_to(tmp_path)
chroot_sources_spec = Path("/") / host_chroot_sources_spec.relative_to(tmp_path)

expected_command = plugin.opts["cmd_base"] + [chroot_sources_spec, chroot_spec]

plugin.buildroot.doChroot.assert_called_once_with(
expected_command,
shell=False,
cwd=chroot_sources,
logger=plugin.buildroot.build_log,
uid=plugin.buildroot.chrootuid,
gid=plugin.buildroot.chrootgid,
user=plugin.buildroot.chrootuser,
unshare_net=not plugin.config.get("rpmbuild_networking", False),
nspawn_args=plugin.config.get("nspawn_args", []),
printOutput=plugin.config.get("print_main_output", True),
)
else:
plugin.buildroot.doChroot.assert_not_called()
if "spec-files-different" in testcase:
log_method = log.warning
else:
log_method = log.debug
log_method.assert_called_once()
log_string = log_method.call_args[0][0]
assert "skipping rpmautospec preprocessing" in log_string

if "without-sources" in testcase:
assert "Sources not specified" in log_string
elif "sources-not-dir" in testcase:
assert "Sources not a directory" in log_string
elif "sources-not-repo" in testcase:
assert "Sources is not a git repository" in log_string
elif "sources-no-specfile" in testcase:
assert "Sources doesn’t contain spec file" in log_string
elif "spec-files-different" in testcase:
assert "Spec file inside and outside sources are different" in log_string
elif "specfile-no-rpmautospec" in testcase:
assert "Spec file doesn’t use rpmautospec" in log_string
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ skipsdist = True
[testenv]
deps =
-rmock/requirements.txt
coverage
pytest
pytest-cov
backoff
setenv =
PYTHONPATH = ./mock/py
commands = python -m pytest -v {posargs} --cov-report term-missing --cov mock/py mock/tests
commands =
python -m pytest -v {posargs} --cov-report term-missing --cov-branch --cov mock/py mock/tests
python -m coverage report --fail-under=100 -m mock/py/mockbuild/plugins/rpmautospec.py

0 comments on commit 6986b10

Please sign in to comment.