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