diff --git a/.packit.yaml b/.packit.yaml
index cc1162c9e..50c95c470 100644
--- a/.packit.yaml
+++ b/.packit.yaml
@@ -14,8 +14,9 @@ jobs:
- fedora-rawhide-aarch64
- mageia-cauldron-x86_64
- mageia-cauldron-aarch64
- - opensuse-tumbleweed-x86_64
- - opensuse-tumbleweed-aarch64
+ # opensuse images are failing because dnf-plugins-core package is not working
+ #- opensuse-tumbleweed-x86_64
+ #- opensuse-tumbleweed-aarch64
trigger: pull_request
- job: copr_build
trigger: commit
@@ -25,8 +26,9 @@ jobs:
- fedora-rawhide-aarch64
- mageia-cauldron-x86_64
- mageia-cauldron-aarch64
- - opensuse-tumbleweed-x86_64
- - opensuse-tumbleweed-aarch64
+ # opensuse images are failing because dnf-plugins-core package is not working
+ #- opensuse-tumbleweed-x86_64
+ #- opensuse-tumbleweed-aarch64
branch: main
project: rpm-software-management-rpmlint-mainline
list_on_homepage: True
@@ -35,8 +37,9 @@ jobs:
trigger: commit
metadata:
targets:
- - opensuse-tumbleweed-x86_64
- - opensuse-tumbleweed-aarch64
+ # opensuse images are failing because dnf-plugins-core package is not working
+ #- opensuse-tumbleweed-x86_64
+ #- opensuse-tumbleweed-aarch64
branch: opensuse
project: rpm-software-management-rpmlint-opensuse
list_on_homepage: True
diff --git a/.packit/rpmlint.spec b/.packit/rpmlint.spec
index 4501ef1fb..b566ef96a 100644
--- a/.packit/rpmlint.spec
+++ b/.packit/rpmlint.spec
@@ -1,7 +1,7 @@
%{!?python3: %global python3 %{__python3}}
Name: rpmlint
-Version: 2.7.0
+Version: 2.8.0
Release: 0%{?dist}
Summary: Tool for checking common errors in RPM packages
diff --git a/configs/openSUSE/opensuse.toml b/configs/openSUSE/opensuse.toml
index 58a3fc9e9..154ece5ac 100644
--- a/configs/openSUSE/opensuse.toml
+++ b/configs/openSUSE/opensuse.toml
@@ -6,6 +6,10 @@ UseVarLockSubsys = false
UseVersionInChangelog = false
BadnessThreshold = 999
+# Set to true to issue a warning for ghost entries outside snapshots
+# when checking for atomic update compatibility
+AtomicCheckGhosts = false
+
# Enabled checks for the rpmlint to be run (besides the default set)
Checks = [
"BashismsCheck",
@@ -24,6 +28,7 @@ Checks = [
"SystemdTmpfilesCheck",
"SUIDPermissionsCheck",
"WorldWritableCheck",
+ "AtomicUpdateCheck",
]
# List of directory prefixes that are not allowed in packages
@@ -31,6 +36,25 @@ DisallowedDirs = [
"/etc/NetworkManager/dispatcher.d",
]
+# Only these directories may be used by packages compatible with
+# atomic updates
+AtomicAllowedDirs = [
+ "/etc/",
+ "/usr/",
+ "/bin/",
+ "/lib/",
+ "/lib64/",
+ "/sbin/",
+ "/boot/",
+]
+
+# List of subdirectories which are disallowed for atomic updates
+# despite being within otherwise allowed directories
+AtomicDisallowedSubdirs = [
+ "/usr/local/",
+ "/boot/efi/",
+]
+
FilterErrorTitles = [
'cross-directory-hard-link',
]
@@ -83,6 +107,7 @@ Filters = [
'^filesystem\..*: dir-or-file-in-tmp',
'^filesystem\..*: dir-or-file-in-mnt',
'^filesystem\..*: dir-or-file-in-home',
+ '^filesystem\..*: dir-or-file-outside-snapshot',
'^filesystem\..*: hidden-file-or-dir /root/.gnupg',
'^filesystem\..*: hidden-file-or-dir /root/.gnupg',
'^filesystem\..*: hidden-file-or-dir /etc/skel/.config',
@@ -270,6 +295,14 @@ BlockedFilters = [
"polkit-untracked-privilege",
"polkit-user-privilege",
"polkit-xml-exception",
+ "sudoers-file-digest-mismatch",
+ "sudoers-file-ghost",
+ "sudoers-file-symlink",
+ "sudoers-file-unauthorized",
+ "sysctl-file-digest-mismatch",
+ "sysctl-file-ghost",
+ "sysctl-file-symlink",
+ "sysctl-file-unauthorized",
"systemd-tmpfile-ghost",
"systemd-tmpfile-symlink",
"systemd-tmpfile-parse-error",
@@ -318,13 +351,15 @@ paths = [
[SystemdTmpfilesWhitelist]
[Descriptions]
-non-standard-uid = '''A file in this package is owned by an unregistered user id.
-To register the user, please make a pull request to the rpmlint config file
-configs/openSUSE/users-groups.toml in the rpmlint repository.
+non-standard-uid = '''A file in this package is owned by an unregistered user
+id. To register the user, please make a pull request to the rpmlint config
+file configs/openSUSE/users-groups.toml in the opensuse branch of the rpmlint
+repository.
'''
-non-standard-gid = '''A file in this package is owned by an unregistered group id.
-To register the group, please make a pull request to the rpmlint config file
-configs/openSUSE/users-groups.toml in the rpmlint repository.
+non-standard-gid = '''A file in this package is owned by an unregistered group
+id. To register the group, please make a pull request to the rpmlint config
+file configs/openSUSE/users-groups.toml in the opensuse branch of the rpmlint
+repository.
'''
no-changelogname-tag = '''There is no changelog. Please insert a '%changelog' section heading in your
spec file and prepare your changes file using e.g. the 'osc vc' command.'''
diff --git a/configs/openSUSE/scoring.toml b/configs/openSUSE/scoring.toml
index a059b2b42..e03810a21 100644
--- a/configs/openSUSE/scoring.toml
+++ b/configs/openSUSE/scoring.toml
@@ -49,6 +49,7 @@ deprecated-boot-script = 10000
executable-stack = 10000
binary-or-shlib-defines-rpath = 10000
patchable-function-entry-in-archive = 10000
+patch-macro-old-format = 10000
pam-file-ghost = 10
pam-file-unauthorized = 10
pam-file-symlink = 10
@@ -98,3 +99,8 @@ missing-hash-section = 10000
zypperplugin-file-digest-mismatch = 10
zypperplugin-file-ghost = 10
zypperplugin-file-unauthorized = 10
+logrotate-user-writable-log-dir = 10000
+
+# Set to 10000 once affected packages have been updated
+# for atomic update compatibility
+dir-or-file-outside-snapshot = 0
diff --git a/pyproject.toml b/pyproject.toml
index 090dc04d5..41ea1a8e6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rpmlint"
-version = "2.7.0"
+version = "2.8.0"
description = "Check for common errors in RPM packages"
license = {text = "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)"}
authors = [
diff --git a/rpmlint/checks/AbstractCheck.py b/rpmlint/checks/AbstractCheck.py
index ffe5f612a..11df015ce 100644
--- a/rpmlint/checks/AbstractCheck.py
+++ b/rpmlint/checks/AbstractCheck.py
@@ -50,9 +50,7 @@ def check_binary(self, pkg):
# start with the biggest files first
filenames = sorted(filenames, key=lambda x: pkg.files[x].size, reverse=True)
with concurrent.futures.ThreadPoolExecutor() as executor:
- futures = []
- for filename in filenames:
- futures.append(executor.submit(self.check_file, pkg, filename))
+ futures = [executor.submit(self.check_file, pkg, filename) for filename in filenames]
concurrent.futures.wait(futures)
for future in futures:
err = future.exception()
diff --git a/rpmlint/checks/AlternativesCheck.py b/rpmlint/checks/AlternativesCheck.py
index 08f2d6c30..e6a830f54 100644
--- a/rpmlint/checks/AlternativesCheck.py
+++ b/rpmlint/checks/AlternativesCheck.py
@@ -22,7 +22,7 @@ class AlternativesCheck(AbstractCheck):
Requires(post) and Requires(postun) must depend on update-alternatives
"""
# Regex to match anything that can be in requires for update-alternatives
- re_requirement = re.compile(r'^(/usr/sbin/|%{?_sbindir}?/)?update-alternatives$')
+ re_requirement = re.compile(r'^(/usr/s?bin/|%{?_s?bindir}?/)?update-alternatives$')
re_install = re.compile(r'--install\s+(?P\S+)\s+(?P\S+)\s+(\S+)\s+(\S+)')
re_slave = re.compile(r'--slave\s+(?P\S+)\s+(\S+)\s+(\S+)')
command = 'update-alternatives'
@@ -220,52 +220,56 @@ def _check_libalternatives_filelist(self, pkg):
Checking content of all /usr/share/libalternatives/*/*.conf files
"""
for f, pkgfile in pkg.files.items():
- if re.search('^/usr/share/libalternatives/.*conf$', f):
- filename = Path(pkg.dirname + f)
- if not filename.exists():
- if pkgfile.is_ghost:
- self.output.add_info('I', pkg, 'libalternatives-conf-not-found', f)
- else:
- self.output.add_info('E', pkg, 'libalternatives-conf-not-found', f)
- continue
- bin_found = False
- man_found = False
- with open(filename) as read_obj:
- # Read all lines in the file one by one. E.g:
- #
- # binary=/usr/bin/jupyter-3.8
- # man=jupyter-3.8.1
- # group=jupyter, jupyter-migrate, jupyter-troubleshoot
- #
- for line_nr, line in enumerate(read_obj):
- line_array = [x.strip() for x in line.split('=')]
- line_nr_str = f'Line: {line_nr}'
- if len(line_array) != 2: # empty values are valid
- self.output.add_info('E', pkg, 'wrong-entry-format', f, line_nr_str)
+ if not re.search(r'^/usr/share/libalternatives/[^/]+/.*\.conf$', f):
+ continue
- key, value = line_array
- if key == 'binary':
- if bin_found:
- self.output.add_info('E', pkg, 'multiple-entries', f, line_nr_str)
- continue
+ filename = Path(pkg.dirname + f)
+ if not filename.exists():
+ if pkgfile.is_ghost:
+ self.output.add_info('I', pkg, 'libalternatives-conf-not-found', f)
+ else:
+ self.output.add_info('E', pkg, 'libalternatives-conf-not-found', f)
+ continue
+
+ bin_found = False
+ man_found = False
+ with open(filename) as read_obj:
+ # Read all lines in the file one by one. E.g:
+ #
+ # binary=/usr/bin/jupyter-3.8
+ # man=jupyter-3.8.1
+ # group=jupyter, jupyter-migrate, jupyter-troubleshoot
+ #
+ for line_nr, line in enumerate(read_obj):
+ line_array = [x.strip() for x in line.split('=')]
+ line_nr_str = f'Line: {line_nr}'
+ if len(line_array) != 2: # empty values are valid
+ self.output.add_info('E', pkg, 'wrong-entry-format', f, line_nr_str)
+ continue
+
+ key, value = line_array
+ if key == 'binary':
+ if bin_found:
+ self.output.add_info('E', pkg, 'multiple-entries', f, line_nr_str)
+ continue
+ for path in pkg.files:
+ if 'bin/' in path and path.endswith(value):
+ bin_found = True
+ if not bin_found:
+ self.output.add_info('W', pkg, 'binary-entry-value-not-found', f, line_nr_str)
+ elif key == 'man':
+ if man_found:
+ self.output.add_info('E', pkg, 'double-entries', f, line_nr_str)
+ continue
+ mans = value.split(',')
+ for man in mans:
+ man_found = False
for path in pkg.files:
- if 'bin/' in path and path.endswith(value):
- bin_found = True
- if not bin_found:
- self.output.add_info('W', pkg, 'binary-entry-value-not-found', f, line_nr_str)
- elif key == 'man':
- if man_found:
- self.output.add_info('E', pkg, 'double-entries', f, line_nr_str)
- continue
- mans = value.split(',')
- for man in mans:
- man_found = False
- for path in pkg.files:
- if path.startswith('/usr/share/man/') and man.strip() in path:
- man_found = True
- if not man_found:
- self.output.add_info('W', pkg, 'man-entry-value-not-found', f, line_nr_str)
- elif key != 'group' and key != 'options':
- self.output.add_info('W', pkg, 'wrong-tag-found', f, line_nr_str)
- if not bin_found:
- self.output.add_info('W', pkg, 'wrong-or-missed-binary-entry', f)
+ if path.startswith('/usr/share/man/') and man.strip() in path:
+ man_found = True
+ if not man_found:
+ self.output.add_info('W', pkg, 'man-entry-value-not-found', f, line_nr_str)
+ elif key != 'group' and key != 'options':
+ self.output.add_info('W', pkg, 'wrong-tag-found', f, line_nr_str)
+ if not bin_found:
+ self.output.add_info('W', pkg, 'wrong-or-missed-binary-entry', f)
diff --git a/rpmlint/checks/AtomicUpdateCheck.py b/rpmlint/checks/AtomicUpdateCheck.py
new file mode 100644
index 000000000..db715d83e
--- /dev/null
+++ b/rpmlint/checks/AtomicUpdateCheck.py
@@ -0,0 +1,44 @@
+from rpmlint.checks.AbstractCheck import AbstractCheck
+
+
+class AtomicUpdateCheck(AbstractCheck):
+
+ """
+ Requirements for atomic updates:
+ * All files must be stored inside the snapshot, which is in our case /etc and /usr, not /var,
+ /opt, /srv, /usr/local or anything else.
+ * (Re)starting daemons is not possible.
+ * Modifying files outside of /usr and /etc is not possible.
+ * Modifications outside the snapshot have to be done via systemd-tmpfiles and systemd services.
+ This check currently only implements checking for files at illegal paths.
+ """
+
+ def __init__(self, config, output):
+ super().__init__(config, output)
+ self.check_ghosts = self.config.configuration['AtomicCheckGhosts']
+ self.allowed_dirs = self.config.configuration['AtomicAllowedDirs']
+ self.disallowed_subdirs = self.config.configuration['AtomicDisallowedSubdirs']
+
+ def check(self, pkg):
+ if pkg.is_source:
+ return
+
+ # Check for files stored outside the snapshot
+ self._check_paths(pkg, self.check_ghosts)
+
+ def _check_paths(self, pkg, check_ghosts=False):
+ for file in pkg.files.keys():
+ if file in pkg.ghost_files:
+ continue # Ghosts are only handled if explicitly desired
+ if not (self._check_single_path(file)):
+ self.output.add_info('E', pkg, 'dir-or-file-outside-snapshot', file)
+ if check_ghosts:
+ for ghost in pkg.ghost_files:
+ if not (self._check_single_path(ghost)):
+ self.output.add_info('W', pkg, 'ghost-outside-snapshot', ghost)
+
+ def _check_single_path(self, file):
+ return (
+ file.startswith(tuple(self.allowed_dirs)) and
+ not file.startswith(tuple(self.disallowed_subdirs))
+ )
diff --git a/rpmlint/checks/BinariesCheck.py b/rpmlint/checks/BinariesCheck.py
index 0e147709a..4d9982d31 100644
--- a/rpmlint/checks/BinariesCheck.py
+++ b/rpmlint/checks/BinariesCheck.py
@@ -147,7 +147,7 @@ def _check_binary_in_etc(self, pkg, bin_name):
We suppose that the package is arch dependent.
"""
- if bin_name.startswith('/etc/'):
+ if bin_name.startswith('/etc/') or bin_name.startswith('/usr/etc/'):
self.output.add_info('E', pkg, 'binary-in-etc', bin_name)
def _check_unstripped_binary(self, bin_name, pkg, pkgfile):
diff --git a/rpmlint/checks/BuildRootAndDateCheck.py b/rpmlint/checks/BuildRootAndDateCheck.py
index c22bbcadd..7b3e7fdb1 100644
--- a/rpmlint/checks/BuildRootAndDateCheck.py
+++ b/rpmlint/checks/BuildRootAndDateCheck.py
@@ -17,12 +17,18 @@ def __init__(self, config, output):
super().__init__(config, output, r'.*')
self.looksliketime = re.compile('(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])')
self.istoday = re.compile(time.strftime('%b %e %Y'))
- self.prepare_regex(rpm.expandMacro('%{?buildroot}') or '^/.*/BUILDROOT/')
+ # in rpm 4.20 the expandMacro will return an empty string and so we run
+ # into the "or" case.
+ # return a string that is modelled after the actual path that rpm 4.20
+ # is using so we can actually match what the rpm 4.20 path will be and
+ # not just any random string in a file that references buildroot.
+ # https://github.com/rpm-software-management/rpm/blob/rpm-4.20.1-release/build/parsePreamble.c#L1290
+ self.prepare_regex(rpm.expandMacro('%{?buildroot}') or '/%{NAME}-%{VERSION}-build/BUILDROOT/')
def prepare_regex(self, buildroot):
for m in ('name', 'version', 'release', 'NAME', 'VERSION', 'RELEASE'):
buildroot = buildroot.replace('%%{%s}' % (m), r'[\w\!-\.]{1,20}')
- self.build_root_re = re.compile(buildroot)
+ self.lookslikebuildroot = re.compile(buildroot)
def check_file(self, pkg, filename):
if filename.startswith('/usr/lib/debug') or pkg.is_source or \
@@ -35,5 +41,5 @@ def check_file(self, pkg, filename):
self.output.add_info('E', pkg, 'file-contains-date-and-time', filename)
else:
self.output.add_info('E', pkg, 'file-contains-current-date', filename)
- if self.build_root_re.search(data):
+ if self.lookslikebuildroot.search(data):
self.output.add_info('E', pkg, 'file-contains-buildroot', filename)
diff --git a/rpmlint/checks/FilesCheck.py b/rpmlint/checks/FilesCheck.py
index 6274c9369..d0ae596f8 100644
--- a/rpmlint/checks/FilesCheck.py
+++ b/rpmlint/checks/FilesCheck.py
@@ -144,7 +144,6 @@
'/var/opt',
'/var/preserve',
'/var/spool',
- '/var/spool/mail',
'/var/tmp',
)
diff --git a/rpmlint/checks/I18NCheck.py b/rpmlint/checks/I18NCheck.py
index 618129b31..60b49ba93 100644
--- a/rpmlint/checks/I18NCheck.py
+++ b/rpmlint/checks/I18NCheck.py
@@ -70,8 +70,7 @@ def is_valid_lang(lang):
class I18NCheck(AbstractCheck):
def check_binary(self, pkg):
- files = list(pkg.files.keys())
- files.sort()
+ files = sorted(pkg.files.keys())
locales = [] # list of locales for this packages
webapp = False
diff --git a/rpmlint/checks/InitScriptCheck.py b/rpmlint/checks/InitScriptCheck.py
index 925a2821d..333734c05 100644
--- a/rpmlint/checks/InitScriptCheck.py
+++ b/rpmlint/checks/InitScriptCheck.py
@@ -42,8 +42,7 @@ def shell_var_value(var, script):
if res2 and res2.group(2) == var: # infinite loop
return None
return substitute_shell_vars(res.group(1), script)
- else:
- return None
+ return None
def substitute_shell_vars(val, script):
@@ -54,8 +53,7 @@ def substitute_shell_vars(val, script):
value = ''
return res.group(1) + value + \
substitute_shell_vars(res.group(3), script)
- else:
- return val
+ return val
class InitScriptCheck(AbstractCheck):
diff --git a/rpmlint/checks/LogrotateCheck.py b/rpmlint/checks/LogrotateCheck.py
index eb06fcb91..cd8bf480b 100644
--- a/rpmlint/checks/LogrotateCheck.py
+++ b/rpmlint/checks/LogrotateCheck.py
@@ -15,7 +15,7 @@ def check(self, pkg):
if f in pkg.ghost_files:
continue
- if f.startswith('/etc/logrotate.d/'):
+ if f.startswith('/etc/logrotate.d/') or f.startswith('/usr/etc/logrotate.d/'):
try:
for n, o in self.parselogrotateconf(pkg.dir_name(), f).items():
if n in dirs and dirs[n] != o:
diff --git a/rpmlint/checks/MenuXDGCheck.py b/rpmlint/checks/MenuXDGCheck.py
index 44c3af640..b8bce6388 100644
--- a/rpmlint/checks/MenuXDGCheck.py
+++ b/rpmlint/checks/MenuXDGCheck.py
@@ -70,10 +70,10 @@ def _handle_parser_error(self, pkg, filename, e):
self.output.add_info('E', pkg, 'desktopfile-missing-header', filename)
elif (isinstance(e, cfgparser.DuplicateSectionError)):
self.output.add_info('E', pkg, 'desktopfile-duplicate-section', filename,
- '[{e.section}]')
+ f'[{e.section}]')
elif (isinstance(e, cfgparser.DuplicateOptionError)):
self.output.add_info('E', pkg, 'desktopfile-duplicate-option', filename,
- '[{e.section}]/{e.option}')
+ f'[{e.section}]/{e.option}')
else:
self.output.add_info('E', pkg, 'invalid-desktopfile', filename,
e.message.partition(':')[0])
diff --git a/rpmlint/checks/SUIDPermissionsCheck.py b/rpmlint/checks/SUIDPermissionsCheck.py
index 27159f1c9..b7da8a338 100644
--- a/rpmlint/checks/SUIDPermissionsCheck.py
+++ b/rpmlint/checks/SUIDPermissionsCheck.py
@@ -81,7 +81,8 @@ def _check_post_scriptlets(self, pkg, path, need_verifyscript):
found = False
if script:
for line in script.split('\n'):
- if '/chkstat' in line and path in line:
+ escaped = re.escape(path)
+ if re.search(fr'(chkstat|permctl) -n .* {escaped}', line):
found = True
break
diff --git a/rpmlint/checks/SpecCheck.py b/rpmlint/checks/SpecCheck.py
index f780926fa..9b0588686 100644
--- a/rpmlint/checks/SpecCheck.py
+++ b/rpmlint/checks/SpecCheck.py
@@ -84,9 +84,12 @@ def re_tag_compile(tag):
tarball_regex = re.compile(r'\.(?:t(?:ar|[glx]z|bz2?)|zip)\b', re.IGNORECASE)
python_setup_test_regex = re.compile(r'^[^#]*(setup.py test)')
+python_setup_install_regex = re.compile(r'^[^#]*(setup.py install|%\{?py(thon)?\d*_install)')
python_module_def_regex = re.compile(r'^[^#]*%{\?!python_module:%define python_module()')
python_sitelib_glob_regex = re.compile(r'^[^#]*%{python_site(lib|arch)}/\*\s*$')
+shared_dir_glob_regex = re.compile(r'^[^#]*%{_(?:bin|data|doc|include|man)dir}/\*\s*$')
+
# %suse_update_desktop_file deprecation
# https://lists.opensuse.org/archives/list/packaging@lists.opensuse.org/message/TF4QO7ECOSEDHBFI5YDEA3OF4RNSI7D7/
suse_update_desktop_file_regex = re.compile(r'^BuildRequires:\s*update-desktop-files', re.IGNORECASE)
@@ -323,8 +326,12 @@ def _check_specfile_error(self, pkg):
for line in outcmd.stderr.splitlines():
line = line.strip()
+ if not line:
+ continue
if line and 'warning:' not in line:
self.output.add_info('E', pkg, 'specfile-error', line)
+ else:
+ self.output.add_info('W', pkg, 'specfile-warning', line)
except UnicodeDecodeError as e:
self.output.add_info('E', pkg, 'specfile-error', str(e))
@@ -390,8 +397,10 @@ def _check_line(self, line):
self._checkline_valid_groups(line)
self._checkline_macros_in_comments(line)
self._checkline_python_setup_test(line)
+ self._checkline_python_setup_install(line)
self._checkline_python_module_def(line)
self._checkline_python_sitelib_glob(line)
+ self._checkline_shared_dir_glob(line)
# If statement, starts
if ifarch_regex.search(line):
@@ -413,6 +422,9 @@ def _checkline_declarative(self, line):
if self.declarative:
return
self.declarative = bool(declarative_regex.search(line))
+ if self.declarative:
+ # we assume implicit %prep
+ self.patches_auto_applied = True
def _checkline_break_space(self, line):
char = line.find(UNICODE_NBSP)
@@ -797,6 +809,11 @@ def _checkline_python_setup_test(self, line):
if self.current_section == 'check' and python_setup_test_regex.search(line):
self.output.add_info('W', self.pkg, 'python-setup-test', line[:-1])
+ def _checkline_python_setup_install(self, line):
+ # Test if the "python setup.py install" deprecated subcommand is used
+ if self.current_section == 'install' and python_setup_install_regex.search(line):
+ self.output.add_info('W', self.pkg, 'python-setup-install', line[:-1])
+
def _checkline_python_module_def(self, line):
"""
Test if the "python_module" macro is defined in the spec file
@@ -815,6 +832,15 @@ def _checkline_python_sitelib_glob(self, line):
self.output.add_info('W', self.pkg, 'python-sitelib-glob-in-files',
line[:-1])
+ def _checkline_shared_dir_glob(self, line):
+ """Test if %{_bindir}/*, etc. is present in %files section."""
+ if self.current_section != 'files':
+ return
+
+ if shared_dir_glob_regex.match(line):
+ self.output.add_info('W', self.pkg, 'shared-dir-glob-in-files',
+ line[:-1])
+
def _checkline_forbidden_controlchars(self, line):
"""Look for controlchar in any line"""
# https://github.com/rpm-software-management/rpmlint/issues/1067
diff --git a/rpmlint/checks/TmpFilesCheck.py b/rpmlint/checks/TmpFilesCheck.py
index ff3bc3f69..407635617 100644
--- a/rpmlint/checks/TmpFilesCheck.py
+++ b/rpmlint/checks/TmpFilesCheck.py
@@ -30,7 +30,6 @@ def check(self, pkg):
continue
self._check_pre_tmpfile(fname, pkg)
- self._check_post_tmpfile(fname, pkg)
self._check_tmpfile_in_filelist(pkgfile, pkg)
def _check_pre_tmpfile(self, fname, pkg):
@@ -49,22 +48,6 @@ def _check_pre_tmpfile(self, fname, pkg):
if pre and tmpfiles_regex.search(pre):
self.output.add_info('W', pkg, 'pre-with-tmpfile-creation', fname)
- def _check_post_tmpfile(self, fname, pkg):
- """
- Check if the %post section contains 'systemd-tmpfiles --create' call.
-
- Print a warning if there is no such call in the %post section.
- """
- post = pkg[rpm.RPMTAG_POSTIN]
-
- basename = Path(fname).name
- tmpfiles_regex = re.compile(r'systemd-tmpfiles --create .*%s'
- % re.escape(basename))
-
- if post and tmpfiles_regex.search(post):
- return
- self.output.add_info('W', pkg, 'post-without-tmpfile-creation', fname)
-
def _check_tmpfile_in_filelist(self, pkgfile, pkg):
"""
Check if the tmpfile is listed in the filelist and marked as %ghost.
diff --git a/rpmlint/checks/ZipCheck.py b/rpmlint/checks/ZipCheck.py
index 3ac48a1bf..3cb1b57b2 100644
--- a/rpmlint/checks/ZipCheck.py
+++ b/rpmlint/checks/ZipCheck.py
@@ -78,7 +78,7 @@ def _check_classpath(self, pkg, fname, jarfile):
return
# otherwise check for the hardcoded classpath
- manifest = jarfile.read(mf).decode()
+ manifest = jarfile.read(mf).decode(errors='replace')
if classpath_regex.search(manifest):
self.output.add_info('W', pkg, 'class-path-in-manifest', fname)
diff --git a/rpmlint/cli.py b/rpmlint/cli.py
index 1f474e906..5d892b090 100644
--- a/rpmlint/cli.py
+++ b/rpmlint/cli.py
@@ -75,7 +75,7 @@ def process_lint_args(argv):
parser.add_argument('-V', '--version', action='version', version=__version__, help='show package version and exit')
parser.add_argument('-c', '--config', type=_validate_conf_location, help='load up additional configuration data from specified path (file or directory with *.toml files)')
parser.add_argument('-e', '--explain', nargs='+', default='', help='provide detailed explanation for one specific message id')
- parser.add_argument('-r', '--rpmlintrc', '--file', type=_is_file_path, help='load up specified rpmlintrc file')
+ parser.add_argument('-r', '--rpmlintrc', '--file', action='append', type=_is_file_path, help='load up specified rpmlintrc file (may be repeated)')
parser.add_argument('-v', '--verbose', '--info', action='store_true', help='provide detailed explanations where available')
parser.add_argument('-p', '--print-config', action='store_true', help='print the settings that are in effect when using the rpmlint')
parser.add_argument('-i', '--installed', nargs='+', default='', help='installed packages to be validated by rpmlint')
@@ -97,16 +97,7 @@ def process_lint_args(argv):
options = parser.parse_args(args=argv)
- # make sure rpmlintrc exists
- if options.rpmlintrc:
- if not options.rpmlintrc.exists():
- print_warning(f"User specified rpmlintrc '{options.rpmlintrc}' does not exist")
- exit(2)
- # make it a list
- options.rpmlintrc = [options.rpmlintrc]
- else:
- options.rpmlintrc = []
- # validate all the rpmlfile options to be either file or folder
+ # validate all the rpmfile options to be either file or folder
f_path = set()
invalid_path = False
for item in options.rpmfile:
@@ -126,7 +117,7 @@ def process_lint_args(argv):
f_path.update(p_path)
if invalid_path:
- exit(2)
+ sys.exit(2)
# convert options to dict
options_dict = vars(options)
# use computed rpmfile
@@ -155,7 +146,7 @@ def _validate_conf_location(string):
if not path.exists():
print_warning(
f"File or dir with user specified configuration '{string}' does not exist")
- exit(2)
+ sys.exit(2)
if path.is_dir():
config_paths.extend(path.glob('*.toml'))
@@ -180,8 +171,7 @@ def lint():
# TODO: remove once OBS integration is done
options = process_lint_args(sys.argv[1:] + ['--permissive'])
- lint = Lint(options)
- sys.exit(lint.run())
+ sys.exit(Lint(options).run())
def diff():
diff --git a/rpmlint/config.py b/rpmlint/config.py
index 957159399..e266c799d 100644
--- a/rpmlint/config.py
+++ b/rpmlint/config.py
@@ -123,10 +123,9 @@ def _sort_config_files(self, config_file):
"""
if config_file == self.config_defaults:
return 0
- elif not self._is_override_config(config_file):
+ if not self._is_override_config(config_file):
return 1
- else:
- return 2
+ return 2
def load_config(self, config=None):
"""
diff --git a/rpmlint/configdefaults.toml b/rpmlint/configdefaults.toml
index a24295daf..e95d25ea6 100644
--- a/rpmlint/configdefaults.toml
+++ b/rpmlint/configdefaults.toml
@@ -36,6 +36,9 @@ Filters = []
BlockedFilters = []
# Treshold where we should error out, by default single error is enough
BadnessThreshold = -1
+# Set to true to issue a warning for ghost entries outside snapshots
+# when checking for atomic update compatibility
+AtomicCheckGhosts = false
# When checking that various files that should be compressed are
# indeed compressed, look for this filename extension
CompressExtension = "bz2"
@@ -213,6 +216,26 @@ DisallowedDirs = [
"/var/run",
"/var/tmp",
]
+
+# Only these directories may be used by packages compatible with
+# atomic updates
+AtomicAllowedDirs = [
+ "/etc/",
+ "/usr/",
+ "/bin/",
+ "/lib/",
+ "/lib64/",
+ "/sbin/",
+ "/boot/",
+]
+
+# List of subdirectories which are disallowed for atomic updates
+# despite being within otherwise allowed directories
+AtomicDisallowedSubdirs = [
+ "/usr/local/",
+ "/boot/efi/",
+]
+
# Standard OS groups
StandardGroups = [
"root",
diff --git a/rpmlint/descriptions/AtomicUpdateCheck.toml b/rpmlint/descriptions/AtomicUpdateCheck.toml
new file mode 100644
index 000000000..2530735c7
--- /dev/null
+++ b/rpmlint/descriptions/AtomicUpdateCheck.toml
@@ -0,0 +1,9 @@
+dir-or-file-outside-snapshot="""
+The package contains files outside the snapshot, e.g. outside /etc and /usr
+or inside /usr/local.
+"""
+ghost-outside-snapshot="""
+The package contains ghosts outside the snapshot, e.g. outside /etc and /usr
+or inside /usr/local. This might become an issue upon removal of this
+package, but not during installation.
+"""
diff --git a/rpmlint/descriptions/SpecCheck.toml b/rpmlint/descriptions/SpecCheck.toml
index 970003879..3225eb4f6 100644
--- a/rpmlint/descriptions/SpecCheck.toml
+++ b/rpmlint/descriptions/SpecCheck.toml
@@ -182,6 +182,10 @@ specfile-error="""
This error occurred when rpmlint used rpm to query the specfile. The error
is output by rpm and the message should contain more information.
"""
+specfile-warning="""
+This warning occurred when rpmlint used rpm to query the specfile. The error
+is output by rpm and the message should contain more information.
+"""
comparison-operator-in-deptoken="""
This dependency token contains a comparison operator (<, > or =). This is
usually not intended and may be caused by missing whitespace between the
@@ -203,6 +207,10 @@ python-setup-test="""
The python setup.py test subcommand is deprecated and should be replaced with a
modern testing tool like %pytest or %pyunittest discover -v.
"""
+python-setup-install="""
+The python setup.py install subcommand is deprecated and should be replaced with
+macros %pyproject_wheel, %pyproject_install or with "pip"
+"""
python-module-def="""
The spec file contains a conditional definition of python_module macro, this
macro is present in recent versions of python-rpm-macros.
@@ -225,6 +233,13 @@ invalid-suse-version-check="""
The specfile contains a comparison of %suse_version against a suse release
that does not exist. Please double check.
"""
+shared-dir-glob-in-files="""
+The %files section contains "%{_bindir}/*", "%{_datadir}/*", "%{_docdir}/*",
+"%{_includedir}/*" or "%{_mandir}/*". These can lead to packagers not noticing
+when upstream adds new and possibly conflicting files in these directories.
+Therefore, files in these directories should be explicitely listed like
+"%{_bindir}/foobar" or "%{_includedir}/foobar.h".
+"""
suse-update-desktop-file-deprecated="""
The usage of %suse_update_desktop_file is deprecated and changes
should be migrated to the upstream.
diff --git a/rpmlint/descriptions/TmpFilesCheck.toml b/rpmlint/descriptions/TmpFilesCheck.toml
index 3b7d433e4..4c49af89c 100644
--- a/rpmlint/descriptions/TmpFilesCheck.toml
+++ b/rpmlint/descriptions/TmpFilesCheck.toml
@@ -2,11 +2,6 @@ pre-with-tmpfile-creation="""
%pre section contains %tmpfiles_create macro that should be in the
%post section instead.
"""
-post-without-tmpfile-creation="""
-Please use the %tmpfiles_create macro in %post for each of your
-tmpfiles.d files if you expect this file or directory to be
-available after package installation (and before reboot).
-"""
tmpfile-not-regular-file="""
Files in tmpfiles.d need to be regular files.
"""
diff --git a/rpmlint/lint.py b/rpmlint/lint.py
index e147d2748..bf7c5f158 100644
--- a/rpmlint/lint.py
+++ b/rpmlint/lint.py
@@ -82,6 +82,7 @@ def _run(self):
if self.options['explain']:
self.print_explanation(self.options['explain'], self.config)
return retcode
+
# if there are installed arguments just load them up as extra
# items to the rpmfile option
if self.options['installed']:
@@ -199,6 +200,7 @@ def _load_rpmlintrc(self):
Load rpmlintrc from argument or load up from folder
"""
if not self.options['rpmlintrc']:
+ self.options['rpmlintrc'] = []
# Skip auto-loading when running under PYTEST
if not os.environ.get('PYTEST_XDIST_TESTRUNUID'):
# first load SUSE-specific locations
@@ -213,11 +215,13 @@ def _load_rpmlintrc(self):
pkg = pkg.parent
self.options['rpmlintrc'] += self._find_rpmlintrc_files(pkg)
- if len(self.options['rpmlintrc']) > 1:
- # multiple rpmlintrcs are highly undesirable
- print_warning('There are multiple items to be loaded: {}.'.format(' '.join(map(str, self.options['rpmlintrc']))))
- for rcfile in self.options['rpmlintrc']:
- self.config.load_rpmlintrc(rcfile)
+ if len(self.options['rpmlintrc']) > 1:
+ # multiple rpmlintrcs are highly undesirable
+ print_warning('There are multiple items to be loaded: {}.'.format(' '.join(map(str, self.options['rpmlintrc']))))
+
+ if self.options['rpmlintrc']:
+ for rcfile in self.options['rpmlintrc']:
+ self.config.load_rpmlintrc(rcfile)
def _print_header(self):
"""
@@ -240,8 +244,10 @@ def _print_header(self):
print('')
def validate_installed_packages(self, packages):
+ # Do not run post checks if there are also plain rpm/spec files to validate
+ run_post_checks = not bool(self.options['rpmfile'])
for pkg in packages:
- self.run_checks(pkg, pkg == packages[-1])
+ self.run_checks(pkg, run_post_checks and pkg == packages[-1])
self.reset_checks()
def validate_files(self, files):
@@ -275,7 +281,7 @@ def _expand_filelist(self, files):
def validate_file(self, pname, is_last):
try:
- if pname.suffix == '.rpm' or pname.suffix == '.spm':
+ if pname.suffix in ('.rpm', '.spm'):
with Pkg(pname, self.config.configuration['ExtractDir'],
verbose=self.config.info) as pkg:
for k, v in pkg.timers.items():
@@ -288,8 +294,7 @@ def validate_file(self, pname, is_last):
print_warning(f'(none): E: fatal error while reading {pname}: {e}')
if self.config.info:
raise e
- else:
- sys.exit(3)
+ sys.exit(3)
def run_checks(self, pkg, is_last):
spec_checks = isinstance(pkg, FakePkg)
diff --git a/rpmlint/pkg.py b/rpmlint/pkg.py
index 65899cb8f..1094854d5 100644
--- a/rpmlint/pkg.py
+++ b/rpmlint/pkg.py
@@ -89,14 +89,13 @@ def compression_algorithm(fname):
fname = str(fname)
if gzip_regex.search(fname):
return gzip
- elif bz2_regex.search(fname):
+ if bz2_regex.search(fname):
return bz2
- elif xz_regex.search(fname):
+ if xz_regex.search(fname):
return lzma
- elif zst_regex.search(fname):
+ if zst_regex.search(fname):
return zstd
- else:
- return None
+ return None
def is_utf8(fname):
@@ -386,42 +385,42 @@ def cleanup(self):
pass
def _calc_magic(self, pkgfile):
- magic = pkgfile.magic
- if not magic:
+ magic_description = pkgfile.magic
+ if not magic_description:
if stat.S_ISDIR(pkgfile.mode):
- magic = 'directory'
+ magic_description = 'directory'
elif stat.S_ISLNK(pkgfile.mode):
- magic = "symbolic link to `%s'" % pkgfile.linkto
+ magic_description = "symbolic link to `%s'" % pkgfile.linkto
elif not pkgfile.size:
- magic = 'empty'
- if not magic and not pkgfile.is_ghost and has_magic:
+ magic_description = 'empty'
+ if not magic_description and not pkgfile.is_ghost and has_magic:
start = time.monotonic()
- magic = get_magic(pkgfile.path)
+ magic_description = get_magic(pkgfile.path)
self.timers['libmagic'] += time.monotonic() - start
- if magic is None or Pkg._magic_from_compressed_re.search(magic):
+ if magic_description is None or Pkg._magic_from_compressed_re.search(magic_description):
# Discard magic from inside compressed files ('file -z')
# until PkgFile gets decompression support. We may get
# such magic strings from package headers already now;
# for example Fedora's rpmbuild as of F-11's 4.7.1 is
# patched so it generates them.
- magic = ''
- return magic
+ magic_description = ''
+ return magic_description
# internal function to gather dependency info used by the above ones
def _gather_aux(self, header, xs, nametag, flagstag, versiontag,
prereq=None):
- names = header[nametag]
- flags = header[flagstag]
versions = header[versiontag]
if versions:
- for loop in range(len(versions)):
- name = byte_to_string(names[loop])
- evr = stringToVersion(byte_to_string(versions[loop]))
- if prereq is not None and flags[loop] & PREREQ_FLAG:
- prereq.append((name, flags[loop] & (~PREREQ_FLAG), evr))
+ names = header[nametag]
+ flags = header[flagstag]
+ for version, name_bytes, flag in zip(versions, names, flags):
+ name = byte_to_string(name_bytes)
+ evr = stringToVersion(byte_to_string(version))
+ if prereq is not None and flag & PREREQ_FLAG:
+ prereq.append((name, flag & (~PREREQ_FLAG), evr))
else:
- xs.append(DepInfo(name, flags[loop], evr))
+ xs.append(DepInfo(name, flag, evr))
return xs, prereq
def _gather_dep_info(self):
@@ -494,14 +493,6 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
- def read_with_mmap(self, filename):
- """Mmap a file, return it's content decoded."""
- try:
- with open(Path(self.dir_name() or '/', filename.lstrip('/'))) as in_file:
- return mmap.mmap(in_file.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ).read().decode()
- except Exception:
- return ''
-
def check_versioned_dep(self, name, version):
# try to match name%_isa as well (e.g. 'foo(x86-64)', 'foo(x86-32)')
name_re = re.compile(r'^%s(\(\w+-\d+\))?$' % re.escape(name))
@@ -513,14 +504,21 @@ def check_versioned_dep(self, name, version):
return True
return False
+ def read_with_mmap(self, filename):
+ """Mmap a file, return it's content decoded."""
+ try:
+ with open(Path(self.dir_name() or '/', filename.lstrip('/'))) as in_file:
+ return mmap.mmap(in_file.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ).read().decode()
+ except Exception:
+ return ''
+
def grep(self, regex, filename):
"""Grep regex from a file, return first matching line number (starting with 1)."""
data = self.read_with_mmap(filename)
match = regex.search(data)
if match:
return data.count('\n', 0, match.start()) + 1
- else:
- return None
+ return None
class Pkg(AbstractPkg):
@@ -589,20 +587,19 @@ def __getitem__(self, key):
val = []
if val == []:
return None
- else:
- # Note that text tags we want to try decoding for real in TagsCheck
- # such as summary, description and changelog are not here.
- if key in (rpm.RPMTAG_NAME, rpm.RPMTAG_VERSION, rpm.RPMTAG_RELEASE,
- rpm.RPMTAG_ARCH, rpm.RPMTAG_GROUP, rpm.RPMTAG_BUILDHOST,
- rpm.RPMTAG_LICENSE, rpm.RPMTAG_HEADERI18NTABLE,
- rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM,
- rpm.RPMTAG_DISTRIBUTION, rpm.RPMTAG_VENDOR) \
- or key in (x[0] for x in SCRIPT_TAGS) \
- or key in (x[1] for x in SCRIPT_TAGS):
- val = byte_to_string(val)
- if key == rpm.RPMTAG_GROUP and val == 'Unspecified':
- val = None
- return val
+ # Note that text tags we want to try decoding for real in TagsCheck
+ # such as summary, description and changelog are not here.
+ if key in (rpm.RPMTAG_NAME, rpm.RPMTAG_VERSION, rpm.RPMTAG_RELEASE,
+ rpm.RPMTAG_ARCH, rpm.RPMTAG_GROUP, rpm.RPMTAG_BUILDHOST,
+ rpm.RPMTAG_LICENSE, rpm.RPMTAG_HEADERI18NTABLE,
+ rpm.RPMTAG_PACKAGER, rpm.RPMTAG_SOURCERPM,
+ rpm.RPMTAG_DISTRIBUTION, rpm.RPMTAG_VENDOR) \
+ or key in (x[0] for x in SCRIPT_TAGS) \
+ or key in (x[1] for x in SCRIPT_TAGS):
+ val = byte_to_string(val)
+ if key == rpm.RPMTAG_GROUP and val == 'Unspecified':
+ val = None
+ return val
# return the name of the directory where the package is extracted
def dir_name(self):
@@ -747,7 +744,6 @@ def get_core_reqs(self):
def get_installed_pkgs(name):
"""Get list of installed package objects by name."""
- pkgs = []
ts = rpm.TransactionSet()
if re.search(r'[?*]|\[.+\]', name):
mi = ts.dbMatch()
@@ -755,10 +751,7 @@ def get_installed_pkgs(name):
else:
mi = ts.dbMatch('name', name)
- for hdr in mi:
- pkgs.append(InstalledPkg(name, hdr))
-
- return pkgs
+ return [InstalledPkg(name, hdr) for hdr in mi]
# Class to provide an API to an installed package
@@ -885,7 +878,7 @@ def create_files(self, files):
"""
# files can be just a list
- if isinstance(files, list) or isinstance(files, tuple):
+ if isinstance(files, (list, tuple)):
for path in files:
self._mock_file(path, {})
# list of files with attributes and content
diff --git a/rpmlint/rpmdiff.py b/rpmlint/rpmdiff.py
index a9e972bd0..c699d51e4 100644
--- a/rpmlint/rpmdiff.py
+++ b/rpmlint/rpmdiff.py
@@ -78,8 +78,7 @@ def __init__(self, old, new, ignore=None, exclude=None):
# compare the files
old_files_dict = self.__fileIteratorToDict(rpm.files(old))
new_files_dict = self.__fileIteratorToDict(rpm.files(new))
- files = list(set(chain(iter(old_files_dict), iter(new_files_dict))))
- files.sort()
+ files = sorted(set(chain(iter(old_files_dict), iter(new_files_dict))))
for f in files:
if self._excluded(f):
diff --git a/test/mockdata/mock_alternatives.py b/test/mockdata/mock_alternatives.py
new file mode 100644
index 000000000..9cc60f18b
--- /dev/null
+++ b/test/mockdata/mock_alternatives.py
@@ -0,0 +1,26 @@
+from Testing import get_tested_mock_package
+
+
+AlternativeConfFolder = get_tested_mock_package(
+ lazyload=True,
+ header={'requires': [], 'POSTIN': '', 'POSTUN': ''},
+ name='alternatives',
+ files={
+ '/usr/share/libalternatives/rst2html/311.conf': {
+ 'create_dirs': True,
+ 'content': 'bin=/usr/bin/rst2html-3.11',
+ },
+ '/usr/share/libalternatives/rst2html/1313.conf': {
+ 'create_dirs': True,
+ 'content': 'binary=/usr/bin/=rst2html-3.13',
+ },
+ '/usr/share/libalternatives/ldaptor-ldap2dhcpconf/311.conf': {
+ 'create_dirs': True,
+ 'content': 'binary=/usr/bin/ldaptor-ldap2dhcpconf-3.11',
+ },
+ '/usr/share/libalternatives/ldaptor-ldap2dhcpconf/1311.conf': {
+ 'create_dirs': True,
+ 'content': 'binary=/usr/bin/ldaptor-ldap2dhcpconf-3.13',
+ },
+ }
+)
diff --git a/test/mockdata/mock_menuxdg.py b/test/mockdata/mock_menuxdg.py
index 5896a9208..53fd2db1b 100644
--- a/test/mockdata/mock_menuxdg.py
+++ b/test/mockdata/mock_menuxdg.py
@@ -30,6 +30,17 @@
Categories=Game;Amusement;
"""
+BADDUP2 = """
+[Desktop Entry]
+Name=rpmlint-test
+Name=name duplicate
+Exec=rpmlint-test file.file
+Icon=chameleon_v_balíku
+Type=Application
+GenericName=rpmlint testcase
+Categories=Game;Amusement;
+"""
+
BADSEC = """
Name=rpmlint-test
@@ -60,6 +71,12 @@
)
+MenuXDGBadDup2Package = get_tested_mock_package(
+ lazyload=True,
+ files={'/usr/share/applications/rpmlint-test.desktop': {'content': BADDUP2}},
+)
+
+
MenuXDGBadSecPackage = get_tested_mock_package(
lazyload=True,
files={'/usr/share/applications/rpmlint-test.desktop': {'content': BADSEC}},
diff --git a/test/spec/bogus-date.spec b/test/spec/bogus-date.spec
new file mode 100644
index 000000000..50d62d6de
--- /dev/null
+++ b/test/spec/bogus-date.spec
@@ -0,0 +1,27 @@
+### Based on macro-in-changelog.spec, I added the date information in changlog, as follows:
+Name: bogus-date
+Version: 0
+Release: 0
+Summary: bogus-date
+License: GPL-2.0-only
+Group: Undefined
+URL: http://rpmlint.zarb.org/#%{name}
+Source0: Source0.tar.gz
+
+%description
+The 'rpm -q --specfile' command will display a warning message if the day of the
+week or date is not set correctly in %changelog.
+For example:'warning: bogus date in %changelog'
+
+%prep
+
+%build
+
+%install
+
+%files
+%{_libdir}/foo
+
+%changelog
+* Wed Oct 22 14:15:39 UTC 2019 - Frank Schreiner
+-
diff --git a/test/spec/libspelling.spec b/test/spec/libspelling.spec
index 517897c54..8491c996f 100644
--- a/test/spec/libspelling.spec
+++ b/test/spec/libspelling.spec
@@ -26,6 +26,7 @@ Summary: A spellcheck library for GTK 4
License: LGPL-2.1-or-later
URL: https://gitlab.gnome.org/chergert/libspelling
Source: %{name}-%{version}.tar.zst
+Patch: dummy.patch
BuildRequires: c_compiler
BuildRequires: meson
diff --git a/test/spec/python-setup-install.spec b/test/spec/python-setup-install.spec
new file mode 100644
index 000000000..0d10e8d7e
--- /dev/null
+++ b/test/spec/python-setup-install.spec
@@ -0,0 +1,29 @@
+Name: python-setup-install
+Version: 1.0
+Release: 0
+Summary: python-setup-install warning
+License: MIT
+URL: https://www.example.com
+Source: Source.tar.gz
+BuildRequires: gcc
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+%description
+A test specfile with python setup.py test that is deprecated.
+
+%prep
+%setup -q
+
+%build
+python3 setup.py build
+
+%install
+python3 setup.py install
+
+%check
+
+%files
+%license COPYING
+%doc ChangeLog README
+
+%changelog
diff --git a/test/spec/python-setup-python_install.spec b/test/spec/python-setup-python_install.spec
new file mode 100644
index 000000000..8dd877672
--- /dev/null
+++ b/test/spec/python-setup-python_install.spec
@@ -0,0 +1,33 @@
+Name: python-setup-python_install
+Version: 1.0
+Release: 0
+Summary: python-setup-install warning
+License: MIT
+URL: https://www.example.com
+Source: Source.tar.gz
+BuildRequires: gcc
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+%description
+A test specfile with python setup.py test that is deprecated.
+
+%prep
+%setup -q
+
+%build
+%python_build
+
+%install
+%python_install
+%python3_install
+%python312_install
+# old fedora version
+%py3_install
+
+%check
+
+%files
+%license COPYING
+%doc ChangeLog README
+
+%changelog
diff --git a/test/spec/shared-bindir-glob-in-files.spec b/test/spec/shared-bindir-glob-in-files.spec
new file mode 100644
index 000000000..8a203002b
--- /dev/null
+++ b/test/spec/shared-bindir-glob-in-files.spec
@@ -0,0 +1,27 @@
+Name: shared-bindir-glob-in-files
+Version: 1.0
+Release: 1%{?dist}
+Summary: Dummy test package
+
+License: Public Domain
+URL: http://fedoraproject.org/
+
+%description
+
+
+%prep
+
+
+%build
+
+
+%install
+
+
+%files
+%{_bindir}/*
+
+
+%changelog
+* Wed Apr 23 2025 Tim Landscheidt
+-
diff --git a/test/spec/shared-datadir-glob-in-files.spec b/test/spec/shared-datadir-glob-in-files.spec
new file mode 100644
index 000000000..1191ba7d1
--- /dev/null
+++ b/test/spec/shared-datadir-glob-in-files.spec
@@ -0,0 +1,27 @@
+Name: shared-datadir-glob-in-files
+Version: 1.0
+Release: 1%{?dist}
+Summary: Dummy test package
+
+License: Public Domain
+URL: http://fedoraproject.org/
+
+%description
+
+
+%prep
+
+
+%build
+
+
+%install
+
+
+%files
+%{_datadir}/*
+
+
+%changelog
+* Wed Apr 23 2025 Tim Landscheidt
+-
diff --git a/test/spec/shared-docdir-glob-in-files.spec b/test/spec/shared-docdir-glob-in-files.spec
new file mode 100644
index 000000000..74091ab2b
--- /dev/null
+++ b/test/spec/shared-docdir-glob-in-files.spec
@@ -0,0 +1,27 @@
+Name: shared-docdir-glob-in-files
+Version: 1.0
+Release: 1%{?dist}
+Summary: Dummy test package
+
+License: Public Domain
+URL: http://fedoraproject.org/
+
+%description
+
+
+%prep
+
+
+%build
+
+
+%install
+
+
+%files
+%{_docdir}/*
+
+
+%changelog
+* Wed Apr 23 2025 Tim Landscheidt
+-
diff --git a/test/spec/shared-includedir-glob-in-files.spec b/test/spec/shared-includedir-glob-in-files.spec
new file mode 100644
index 000000000..66901d139
--- /dev/null
+++ b/test/spec/shared-includedir-glob-in-files.spec
@@ -0,0 +1,27 @@
+Name: shared-includedir-glob-in-files
+Version: 1.0
+Release: 1%{?dist}
+Summary: Dummy test package
+
+License: Public Domain
+URL: http://fedoraproject.org/
+
+%description
+
+
+%prep
+
+
+%build
+
+
+%install
+
+
+%files
+%{_includedir}/*
+
+
+%changelog
+* Wed Apr 23 2025 Tim Landscheidt
+-
diff --git a/test/spec/shared-mandir-glob-in-files.spec b/test/spec/shared-mandir-glob-in-files.spec
new file mode 100644
index 000000000..58881c6f3
--- /dev/null
+++ b/test/spec/shared-mandir-glob-in-files.spec
@@ -0,0 +1,27 @@
+Name: shared-mandir-glob-in-files
+Version: 1.0
+Release: 1%{?dist}
+Summary: Dummy test package
+
+License: Public Domain
+URL: http://fedoraproject.org/
+
+%description
+
+
+%prep
+
+
+%build
+
+
+%install
+
+
+%files
+%{_mandir}/*
+
+
+%changelog
+* Wed Apr 23 2025 Tim Landscheidt
+-
diff --git a/test/test_alternatives.py b/test/test_alternatives.py
index e12669e21..b3fffa913 100644
--- a/test/test_alternatives.py
+++ b/test/test_alternatives.py
@@ -1,3 +1,4 @@
+from mockdata.mock_alternatives import AlternativeConfFolder
import pytest
from rpmlint.checks.AlternativesCheck import AlternativesCheck
from rpmlint.filter import Filter
@@ -85,3 +86,14 @@ def test_libalternative_borked(tmp_path, package, alternativescheck):
assert 'E: empty-libalternatives-directory' in out
assert 'W: man-entry-value-not-found' in out
assert 'W: binary-entry-value-not-found' in out
+
+
+@pytest.mark.parametrize('package', [AlternativeConfFolder])
+def test_alternative_conf_folder(package, alternativescheck):
+ output, test = alternativescheck
+ test.check(package)
+ out = output.print_results(output.results)
+ assert 'E: libalternatives-conf-not-found' not in out
+ assert 'W: wrong-tag-found' in out
+ assert 'E: wrong-entry-format' in out
+ assert 'W: binary-entry-value-not-found' in out
diff --git a/test/test_atomic_update.py b/test/test_atomic_update.py
new file mode 100644
index 000000000..bf47c124a
--- /dev/null
+++ b/test/test_atomic_update.py
@@ -0,0 +1,67 @@
+import pytest
+import rpm
+from rpmlint.checks.AtomicUpdateCheck import AtomicUpdateCheck
+from rpmlint.filter import Filter
+
+from Testing import CONFIG, get_tested_mock_package
+
+
+@pytest.fixture(scope='function', autouse=True)
+def atomiccheck():
+ CONFIG.info = True
+ CONFIG.configuration['AtomicCheckGhosts'] = True
+ output = Filter(CONFIG)
+ test = AtomicUpdateCheck(CONFIG, output)
+ yield output, test
+
+
+@pytest.fixture
+def output(atomiccheck):
+ output, _test = atomiccheck
+ yield output
+
+
+@pytest.fixture
+def test(atomiccheck):
+ _output, test = atomiccheck
+ yield test
+
+
+@pytest.mark.parametrize('package', [
+ get_tested_mock_package(files=('/var/lib/pipewire',)),
+ get_tested_mock_package(files=('/opt/bin/test',)),
+ get_tested_mock_package(files=('/usr/local/bin/test',)),
+ get_tested_mock_package(files=('/boot/efi/test',)),
+])
+def test_not_atomic(package, output, test):
+ test.check(package)
+ out = output.print_results(output.results)
+ assert 'E: dir-or-file-outside-snapshot' in out
+
+
+@pytest.mark.parametrize('package', [
+ get_tested_mock_package(files=('/etc/custom.config',)),
+ get_tested_mock_package(files=('/usr/lib64/libc.so',)),
+ get_tested_mock_package(files=('/usr/etc/nfs.conf',)),
+ get_tested_mock_package(files=('/bin/test',)),
+ get_tested_mock_package(files=('/sbin/test',)),
+ get_tested_mock_package(files=('/lib/libc.so',)),
+ get_tested_mock_package(files=('/lib64/libc.so',)),
+ get_tested_mock_package(files=('/boot/grub2/grub.cfg',)),
+])
+def test_atomic(package, output, test):
+ test.check(package)
+ out = output.print_results(output.results)
+ assert 'E: dir-or-file-outside-snapshot' not in out
+ assert 'W: ghost-outside-snapshot' not in out
+
+
+@pytest.mark.parametrize('package', [
+ get_tested_mock_package(files={
+ '/var/lib/pipewire/ghost_file': {'metadata': {'flags': rpm.RPMFILE_GHOST}},
+ }),
+])
+def test_not_atomic_ghost(package, output, test):
+ test.check(package)
+ out = output.print_results(output.results)
+ assert 'W: ghost-outside-snapshot' in out
diff --git a/test/test_cli.py b/test/test_cli.py
index 1544f302a..c7363ae8c 100644
--- a/test/test_cli.py
+++ b/test/test_cli.py
@@ -1,11 +1,12 @@
from pathlib import PosixPath
+from unittest.mock import Mock
import pytest
from rpmlint.cli import process_lint_args
from rpmlint.config import Config
from rpmlint.lint import Lint
-from Testing import HAS_CHECKBASHISMS, HAS_DASH
+from Testing import HAS_CHECKBASHISMS, HAS_DASH, HAS_RPMDB
@pytest.mark.parametrize('test_arguments', [['-c', 'rpmlint/configs/thisdoesntexist.toml']])
@@ -91,3 +92,18 @@ def test_reset_check():
lint.run()
out = lint.output.print_results(lint.output.results, lint.config)
assert 'more-than-one-%changelog-section' not in out
+
+
+@pytest.mark.skipif(not HAS_RPMDB, reason='No RPM database present')
+@pytest.mark.parametrize('args', [
+ ['test/spec/SpecCheck2.spec', 'test/spec/SpecCheck3.spec'],
+ ['-i', 'rpm', 'glibc'],
+ ['test/spec/SpecCheck2.spec', '-i', 'rpm'],
+ ['test/spec/SpecCheck2.spec', 'test/spec/SpecCheck3.spec', '-i', 'rpm', 'glibc'],
+])
+def test_validate_filters(args):
+ options = process_lint_args(args)
+ lint = Lint(options)
+ lint.output.validate_filters = Mock(wraps=lint.output.validate_filters)
+ lint.run()
+ lint.output.validate_filters.assert_called_once()
diff --git a/test/test_files.py b/test/test_files.py
index 402aaf6d0..f674587a7 100644
--- a/test/test_files.py
+++ b/test/test_files.py
@@ -153,7 +153,7 @@ def test_makefile_junk(package, filescheck):
def test_sphinx_inv_files(package, filescheck):
output, test = filescheck
test.check(package)
- assert not len(output.results)
+ assert not output.results
@pytest.mark.parametrize('package', [FileChecksPackage])
@@ -257,9 +257,9 @@ def test_shlib(package, is_devel, filescheck):
assert 'library-without-ldconfig-postin' in out
assert 'library-without-ldconfig-postun' in out
if is_devel:
- assert ('non-devel-file-in-devel-package' in out)
+ assert 'non-devel-file-in-devel-package' in out
else:
- assert ('devel-file-in-non-devel-package' in out)
+ assert 'devel-file-in-non-devel-package' in out
@pytest.mark.parametrize('package, files', [
diff --git a/test/test_lint.py b/test/test_lint.py
index 24a202af6..59b7ca84d 100644
--- a/test/test_lint.py
+++ b/test/test_lint.py
@@ -392,10 +392,7 @@ def test_run_full_specs(capsys, packages, configs):
@pytest.mark.no_cover
def test_run_full_directory(capsys, packages):
assert packages.is_dir()
- file_list = []
- for item in packages.iterdir():
- if item.is_file():
- file_list.append(item)
+ file_list = [item for item in packages.iterdir() if item.is_file()]
number_of_pkgs = len(file_list)
additional_options = {
'rpmfile': [packages],
diff --git a/test/test_menuxdg.py b/test/test_menuxdg.py
index 95f3aa7a8..377ca5bc9 100644
--- a/test/test_menuxdg.py
+++ b/test/test_menuxdg.py
@@ -1,5 +1,6 @@
from mockdata.mock_menuxdg import (
MenuXDGBadBinPackage,
+ MenuXDGBadDup2Package,
MenuXDGBadDupPackage,
MenuXDGBadSecPackage,
MenuXDGBadUTF8Package,
@@ -49,6 +50,19 @@ def test_duplicate(package, menuxdgcheck):
test.check(package)
out = output.print_results(output.results)
assert 'desktopfile-duplicate-section' in out
+ assert '[{e.section}]' not in out
+ assert 'invalid-desktopfile' in out
+
+
+@pytest.mark.skipif(not HAS_DESKTOP_FILE_UTILS, reason='Optional dependency desktop-file-utils not installed')
+@pytest.mark.parametrize('package', [MenuXDGBadDup2Package])
+def test_duplicate_option(package, menuxdgcheck):
+ output, test = menuxdgcheck
+ test.check(package)
+ out = output.print_results(output.results)
+ assert 'desktopfile-duplicate-option' in out
+ assert '[{e.section}]' not in out
+ assert '{e.option}' not in out
assert 'invalid-desktopfile' in out
diff --git a/test/test_signature.py b/test/test_signature.py
index 18b45d2fd..f2d35dc57 100644
--- a/test/test_signature.py
+++ b/test/test_signature.py
@@ -1,3 +1,5 @@
+import re
+
import pytest
from rpmlint.checks.SignatureCheck import SignatureCheck
from rpmlint.filter import Filter
@@ -31,7 +33,8 @@ def test_unknown_key(tmp_path, package, signaturecheck):
output, test = signaturecheck
test.check(get_tested_package(package, tmp_path))
out = output.print_results(output.results)
- assert 'E: unknown-key 31fdc502' in out
+ # https://github.com/rpm-software-management/rpm/issues/2403
+ assert re.search(r'E: unknown-key (84944291)?31fdc502\b', out)
assert 'E: no-signature' not in out
assert 'E: invalid-signature' not in out
diff --git a/test/test_speccheck.py b/test/test_speccheck.py
index f41930d88..a981027c6 100644
--- a/test/test_speccheck.py
+++ b/test/test_speccheck.py
@@ -914,6 +914,7 @@ def test_check_no_essential_section_declarative(package, speccheck):
assert 'W: no-%install-section' not in out
assert 'W: no-%build-section' not in out
assert 'W: no-%check-section' not in out
+ assert 'W: patch-not-applied' not in out
@pytest.mark.parametrize('package', ['spec/SpecCheck2'])
@@ -1169,6 +1170,21 @@ def test_python_setup_test(package, speccheck):
assert 'W: python-setup-test' in out
+@pytest.mark.parametrize('package,lines', [
+ ('spec/python-setup-install', ['setup.py install']),
+ ('spec/python-setup-python_install', ['python_install', 'python3_install', 'python312_install', 'py3_install']),
+])
+def test_python_setup_install(package, lines, speccheck):
+ """Test if specfile has deprecated use of 'setup.py install'."""
+ output, test = speccheck
+ pkg = get_tested_spec_package(package)
+ test.check_spec(pkg)
+ out = output.print_results(output.results)
+ assert 'W: python-setup-install' in out
+ for line in lines:
+ assert line in out
+
+
@pytest.mark.parametrize('package', ['spec/python-module-def'])
def test_python_module_definition(package, speccheck):
"""Test if python_module macro is defined in the spec file."""
@@ -1231,6 +1247,22 @@ def test_suse_version(package, speccheck):
assert 'E: invalid-suse-version-check 56789' in out
+@pytest.mark.parametrize('package', [
+ 'spec/shared-bindir-glob-in-files',
+ 'spec/shared-datadir-glob-in-files',
+ 'spec/shared-docdir-glob-in-files',
+ 'spec/shared-includedir-glob-in-files',
+ 'spec/shared-mandir-glob-in-files',
+])
+def test_shared_dir_glob(package, speccheck):
+ """Test if %{_bindir}/*, etc. is present in %files section."""
+ output, test = speccheck
+ pkg = get_tested_spec_package(package)
+ test.check_spec(pkg)
+ out = output.print_results(output.results)
+ assert 'W: shared-dir-glob-in-files' in out
+
+
@pytest.mark.parametrize('package', [
'spec/null-char-last',
'spec/null-char-first',
@@ -1265,3 +1297,12 @@ def test_deprecated_suse_update_desktop_files(spec, expected, output, test):
test.check_spec(package)
out = output.print_results(output.results)
assert ('W: suse-update-desktop-file-deprecated' in out) == expected
+
+
+@pytest.mark.parametrize('spec', ['spec/bogus-date'])
+def test_bogus_date_in_changelog(spec, output, test):
+ package = get_tested_spec_package(spec)
+ test.check_spec(package)
+ out = output.print_results(output.results)
+ assert 'warning: bogus date in %changelog' in out
+ assert 'W: specfile-warning warning: bogus date in %changelog' in out
diff --git a/test/test_spellchecking.py b/test/test_spellchecking.py
index f557db4c3..3e97f3cf6 100644
--- a/test/test_spellchecking.py
+++ b/test/test_spellchecking.py
@@ -51,7 +51,7 @@ def test_spellchecking():
result = spell.spell_check(text, 'Description({}):')
assert len(result) == 2
assert result['tihs'].startswith('Description(en_US): tihs -> ')
- assert get_suggestions(result['tihs']) == ['hits', 'this', 'ties']
+ assert 'this' in get_suggestions(result['tihs'])
# different language, one typo
text = 'Příčerně žluťoučký kůň'
diff --git a/test/test_suid_permissions.py b/test/test_suid_permissions.py
index 4fc272582..3207735cf 100644
--- a/test/test_suid_permissions.py
+++ b/test/test_suid_permissions.py
@@ -155,6 +155,11 @@ def test_permissions_d(tmp_path, package, permissions_check):
if [ -x /usr/bin/permctl ]; then \
/usr/bin/permctl -n --set --system /var/lib/perms/test || : \
fi \
+""",
+ 'VERIFYSCRIPT': """
+ if [ -x /usr/bin/permctl ]; then \
+ /usr/bin/permctl -n --set --system /var/lib/perms/test || : \
+ fi \
""",
},
)
@@ -164,6 +169,11 @@ def test_permissions_d(tmp_path, package, permissions_check):
if [ -x /usr/bin/chkstat ]; then \
/usr/bin/chkstat -n --set --system /var/lib/perms/test || : \
fi \
+""",
+ 'VERIFYSCRIPT': """
+ if [ -x /usr/bin/chkstat ]; then \
+ /usr/bin/chkstat -n --set --system /var/lib/perms/test || : \
+ fi \
""",
},
)
@@ -175,3 +185,4 @@ def test_permissions_permctl(package, permissions_check):
test.check(package)
out = output.print_results(output.results)
assert 'permissions-missing-postin' not in out
+ assert 'permissions-missing-verifyscript' not in out
diff --git a/test/test_tmp_files.py b/test/test_tmp_files.py
index efc417a2f..c51bfb823 100644
--- a/test/test_tmp_files.py
+++ b/test/test_tmp_files.py
@@ -25,7 +25,6 @@ def test_tmpfiles(package, tmpfilescheck):
out = output.print_results(output.results)
assert 'W: pre-with-tmpfile-creation ' not in out
- assert 'W: post-without-tmpfile-creation /usr/lib/tmpfiles.d/krb5.conf' in out
assert 'W: tmpfile-not-in-filelist /var/lib/kerberos' in out
assert 'W: tmpfile-not-regular-file /usr/lib/tmpfiles.d/symlink.conf' in out
@@ -37,7 +36,6 @@ def test_tmpfiles2(package, tmpfilescheck):
out = output.print_results(output.results)
assert 'W: pre-with-tmpfile-creation /usr/lib/tmpfiles.d/systemd-tmpfiles.conf' in out
- assert 'W: post-without-tmpfile-creation' in out
assert 'W: tmpfile-not-in-filelist /run/my_new_directory' in out
assert 'W: tmpfile-not-regular-file' not in out
@@ -49,6 +47,5 @@ def test_tmpfiles_correct(package, tmpfilescheck):
out = output.print_results(output.results)
assert 'W: pre-with-tmpfile-creation' not in out
- assert 'W: post-without-tmpfile-creation' not in out
assert 'W: tmpfile-not-regular-file' not in out
assert 'W: tmpfile-not-in-filelist' not in out