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 9, 2023
1 parent bb763d6 commit 9d613c1
Show file tree
Hide file tree
Showing 9 changed files with 316 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.
113 changes: 113 additions & 0 deletions mock/py/mockbuild/plugins/rpmautospec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-

Check warning

Code scanning / vcs-diff-lint

Missing module docstring Warning

Missing module docstring
# 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]>
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):

Check warning

Code scanning / vcs-diff-lint

init: Missing function or method docstring Warning

init: Missing function or method docstring
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(

Check warning

Code scanning / vcs-diff-lint

RpmautospecPlugin.attempt_process_distgit: Missing function or method docstring Warning

RpmautospecPlugin.attempt_process_distgit: Missing function or method docstring
self,
host_chroot_spec: Union[Path, str],
host_chroot_sources: Optional[Union[Path, str]],
) -> None:
# 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),
)
2 changes: 2 additions & 0 deletions mock/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ distro
jinja2
pyroute2
requests
# This can be changed to plain `rpmautospec-core` once it’s available on PyPI:
https://github.com/fedora-infra/rpmautospec-core/releases/download/0.1.0/rpmautospec_core-0.1.0-py3-none-any.whl
templated-dictionary
Empty file added mock/tests/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions mock/tests/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
try:

Check warning

Code scanning / vcs-diff-lint

Missing module docstring Warning test

Missing module docstring
from contextlib import nullcontext

Check warning

Code scanning / vcs-diff-lint

Unused nullcontext imported from contextlib Warning test

Unused nullcontext imported from contextlib
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.
177 changes: 177 additions & 0 deletions mock/tests/plugins/test_rpmautospec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
from copy import deepcopy

Check warning

Code scanning / vcs-diff-lint

Missing module docstring Warning test

Missing module docstring
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):

Check warning

Code scanning / vcs-diff-lint

test_init: Missing function or method docstring Warning test

test_init: Missing function or method docstring

Check warning

Code scanning / vcs-diff-lint

test_init: Argument name "RpmautospecPlugin" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern Warning test

test_init: Argument name "RpmautospecPlugin" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern
plugins = object()
conf = object()
buildroot = object()

rpmautospec.init(plugins, conf, buildroot)

RpmautospecPlugin.assert_called_once_with(plugins, conf, buildroot)


class TestRpmautospecPlugin:

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin: Missing class docstring Warning test

TestRpmautospecPlugin: Missing class docstring
DEFAULT_OPTS = {
"requires": ["rpmautospec"],
"cmd_base": ["rpmautospec", "process-distgit"],
}

def create_plugin(self, plugins=UNSET, conf=UNSET, buildroot=UNSET):

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.create_plugin: Missing function or method docstring Warning test

TestRpmautospecPlugin.create_plugin: Missing function or method docstring
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):

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test___init__: Missing function or method docstring Warning test

TestRpmautospecPlugin.test___init__: Missing function or method docstring
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):

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test_attempt_process_distgit: Missing function or method docstring Warning test

TestRpmautospecPlugin.test_attempt_process_distgit: Missing function or method docstring

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test_attempt_process_distgit: Too many branches (21/12) Warning test

TestRpmautospecPlugin.test_attempt_process_distgit: Too many branches (21/12)

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test_attempt_process_distgit: Too many statements (62/50) Warning test

TestRpmautospecPlugin.test_attempt_process_distgit: Too many statements (62/50)
# 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"

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test_attempt_process_distgit: Variable name "SPEC_DIR" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern Warning test

TestRpmautospecPlugin.test_attempt_process_distgit: Variable name "SPEC_DIR" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern
SPEC_DIR.mkdir()
SOURCES_DIR = tmp_path / "SOURCES"

Check warning

Code scanning / vcs-diff-lint

TestRpmautospecPlugin.test_attempt_process_distgit: Variable name "SOURCES_DIR" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern Warning test

TestRpmautospecPlugin.test_attempt_process_distgit: Variable name "SOURCES_DIR" doesn't conform to '[a-z_][a-zA-Z0-9_]{,30}$' pattern
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 9d613c1

Please sign in to comment.