• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rpm-software-management / rpmlint / 27743531775

18 Jun 2026 07:23AM UTC coverage: 88.143% (+0.2%) from 87.933%
27743531775

push

github

6096 of 6916 relevant lines covered (88.14%)

3.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.25
/rpmlint/checks/SpecCheck.py
1
from pathlib import Path
4✔
2
import re
4✔
3
import subprocess
4✔
4
from urllib.parse import urlparse
4✔
5

6
import rpm
4✔
7
from rpmlint import pkg as Pkg
4✔
8
from rpmlint.checks.AbstractCheck import AbstractCheck
4✔
9
from rpmlint.helpers import ENGLISH_ENVIRONMENT, readlines
4✔
10

11
# Don't check for hardcoded library paths in biarch packages
12
DEFAULT_BIARCH_PACKAGES = '^(gcc|glibc)'
4✔
13

14

15
def re_tag_compile(tag):
4✔
16
    rpm_tag = fr'^{tag}\s*:\s*(\S.*?)\s*$'
4✔
17
    return re.compile(rpm_tag, re.IGNORECASE)
4✔
18

19

20
patch_regex = re_tag_compile(r'Patch(\d*)')
4✔
21
# rpm 4.20 doesn't support %patchN anymore, we should warn about this
22
applied_patch_rpm420_regex = re.compile(r'^%patch(\d+)')
4✔
23
applied_patch_regex = re.compile(r'^%patch\s*(\d*)')
4✔
24
applied_patch_p_regex = re.compile(r'\s-P\s*(\d+)\b')
4✔
25
applied_patch_pipe_regex = re.compile(r'\s%\{PATCH(\d+)\}\s*\|\s*(%\{?__)?patch\b')
4✔
26
applied_patch_i_regex = re.compile(r'(?:%\{?__)?patch\}?.*?\s+(?:<|-i)\s+%\{PATCH(\d+)\}')
4✔
27
source_dir_regex = re.compile(r'^[^#]*(\$RPM_SOURCE_DIR|%{?_sourcedir}?)')
4✔
28
obsolete_tags_regex = re_tag_compile(r'(?:Serial|Copyright)')
4✔
29
buildroot_regex = re_tag_compile('BuildRoot')
4✔
30
prefix_regex = re_tag_compile('Prefix')
4✔
31
packager_regex = re_tag_compile('Packager')
4✔
32
buildarch_regex = re_tag_compile('BuildArch(?:itectures)?')
4✔
33
buildprereq_regex = re_tag_compile('BuildPreReq')
4✔
34
prereq_regex = re_tag_compile(r'PreReq(\(.*\))')
4✔
35

4✔
36
make_check_regex = re.compile(r'(^|\s|%{?__)make}?\s+(check|test)')
37
rm_regex = re.compile(r'(^|\s)((.*/)?rm|%{?__rm}?) ')
4✔
38
rpm_buildroot_regex = re.compile(r'^[^#]*?(?:(\\*)\${?RPM_BUILD_ROOT}?|(%+){?buildroot}?)')
4✔
39
configure_libdir_spec_regex = re.compile(r'ln |\./configure[^#]*--libdir=(\S+)[^#]*')
4✔
40
lib_package_regex = re.compile(r'^%package.*\Wlib')
4✔
41
ifarch_regex = re.compile(r'^\s*%ifn?arch\s')
4✔
42
if_regex = re.compile(r'^\s*%if\s')
4✔
43
endif_regex = re.compile(r'^\s*%endif\b')
4✔
44
biarch_package_regex = re.compile(DEFAULT_BIARCH_PACKAGES)
4✔
45
libdir_regex = re.compile(r'%{?_lib(?:dir)?\}?\b')
4✔
46
section_regexs = {x: re.compile('^%' + x + r'(?:\s|$)')
4✔
47
                  for x in ('build', 'changelog', 'check', 'clean', 'description', 'files',
4✔
48
                            'install', 'package', 'prep') + Pkg.RPM_SCRIPTLETS}
49
deprecated_grep_regex = re.compile(r'\b[ef]grep\b')
50

4✔
51
# Only check for /lib, /usr/lib, /usr/X11R6/lib
52
# TODO: better handling of X libraries and modules.
53
hardcoded_library_paths = '(/lib|/usr/lib|/usr/X11R6/lib/(?!([^/]+/)+)[^/]*\\.([oa]|la|so[0-9.]*))'
54
hardcoded_library_path_regex = re.compile(r'^[^#]*((^|\s+|\.\./\.\.|\${?RPM_BUILD_ROOT}?|%{?buildroot}?|%{?_prefix}?)' + hardcoded_library_paths + r'(?=[\s;/])([^\s,;]*))')
4✔
55

4✔
56
DEFINE_RE = r'(^|\s)%(define|global)\s+'
57
depscript_override_regex = re.compile(DEFINE_RE + r'__find_(requires|provides)\s')
4✔
58
depgen_disable_regex = re.compile(DEFINE_RE + r'_use_internal_dependency_generator\s+0')
4✔
59
patch_fuzz_override_regex = re.compile(DEFINE_RE + r'_default_patch_fuzz\s+(\d+)')
4✔
60

4✔
61
# See https://bugzilla.redhat.com/488146 for details
62
indent_spaces_regex = re.compile('( \t|(^|\t)([^\t]{8})*[^\t]{4}[^\t]?([^\t][^\t.!?]|[^\t]?[.!?] )  )')
63

4✔
64
requires_regex = re.compile(r'^(?:Build)?(?:Pre)?Req(?:uires)?(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE)
65
provides_regex = re.compile(r'^Provides(?:\([^\)]+\))?:\s*(.*)', re.IGNORECASE)
4✔
66
obsoletes_regex = re.compile(r'^Obsoletes:\s*(.*)', re.IGNORECASE)
4✔
67
conflicts_regex = re.compile(r'^(?:Build)?Conflicts:\s*(.*)', re.IGNORECASE)
4✔
68

4✔
69
declarative_regex = re.compile(r'^BuildSystem:\s*(.*)', re.IGNORECASE)
70

4✔
71
compop_regex = re.compile(r'[<>=]')
72

4✔
73
setup_regex = re.compile(r'%setup\b')  # intentionally no whitespace before!
74
setup_q_regex = re.compile(r' -[A-Za-z]*q')
4✔
75
setup_t_regex = re.compile(r' -[A-Za-z]*T')
4✔
76
setup_ab_regex = re.compile(r' -[A-Za-z]*[ab]')
4✔
77
autosetup_regex = re.compile(r'^\s*%autosetup(\s.*|$)')
4✔
78
autosetup_n_regex = re.compile(r' -[A-Za-z]*N')
4✔
79
autopatch_regex = re.compile(r'^\s*%autopatch(?:\s|$)')
4✔
80

4✔
81
filelist_regex = re.compile(r'\s+-f\s+\S+')
82
pkgname_regex = re.compile(r'\s+(?:-n\s+)?(\S+)')
4✔
83
tarball_regex = re.compile(r'\.(?:t(?:ar|[glx]z|bz2?)|zip)\b', re.IGNORECASE)
4✔
84

4✔
85
python_setup_test_regex = re.compile(r'^[^#]*(setup.py test)')
86
python_setup_install_regex = re.compile(r'^[^#]*(setup.py install|%\{?py(thon)?\d*_install)')
4✔
87
python_module_def_regex = re.compile(r'^[^#]*%{\?!python_module:%define python_module()')
4✔
88
python_sitelib_glob_regex = re.compile(r'^[^#]*%{python_site(lib|arch)}/\*\s*$')
4✔
89

4✔
90
shared_dir_glob_regex = re.compile(r'^[^#]*%{_(?:bin|data|doc|include|man)dir}/\*\s*$')
91

4✔
92
# %suse_update_desktop_file deprecation
93
# https://lists.opensuse.org/archives/list/packaging@lists.opensuse.org/message/TF4QO7ECOSEDHBFI5YDEA3OF4RNSI7D7/
94
suse_update_desktop_file_regex = re.compile(r'^BuildRequires:\s*update-desktop-files', re.IGNORECASE)
95

4✔
96
UNICODE_NBSP = '\xa0'
97

4✔
98

99
def unversioned(deps):
100
    """Yield unversioned dependency names from the given list."""
4✔
101
    for dep in deps:
102
        if not dep[1]:
4✔
103
            yield dep[0]
4✔
104

4✔
105

106
def contains_buildroot(line):
107
    """Check if the given line contains use of rpm buildroot."""
4✔
108
    res = rpm_buildroot_regex.search(line)
109
    if res and \
4✔
110
       (not res.group(1) or len(res.group(1)) % 2 == 0) and \
4✔
111
       (not res.group(2) or len(res.group(2)) % 2 != 0):
112
        return True
113
    return False
4✔
114

4✔
115

116
class SpecCheck(AbstractCheck):
117
    """Contain check methods that catch errors and warnings in a specfile."""
4✔
118

119
    def __init__(self, config, output):
120
        super().__init__(config, output)
4✔
121
        self._spec_file = None
4✔
122
        self._spec_name = None
4✔
123
        self.valid_groups = config.configuration['ValidGroups']
4✔
124
        self.output.error_details.update({'non-standard-group':
4✔
125
                                         """The value of the Group tag in the package is not valid.  Valid groups are:
4✔
126
                                         '%s'.""" % ', '.join(self.valid_groups)})
4✔
127
        self.hardcoded_lib_path_exceptions_regex = re.compile(config.configuration['HardcodedLibPathExceptions'])
128

129
        self._default_state()
4✔
130

131
    def _default_state(self):
4✔
132
        # Default state
133
        self.patches = {}
4✔
134
        self.applied_patches = []
135
        self.applied_patches_ifarch = []
4✔
136
        self.patches_auto_applied = False
4✔
137
        self.source_dir = False
4✔
138
        self.buildroot = False
4✔
139
        self.configure_linenum = None
4✔
140
        self.configure_cmdline = ''
4✔
141
        self.mklibname = False
4✔
142
        self.is_lib_pkg = False
4✔
143
        self.if_depth = 0
4✔
144
        self.ifarch_depth = -1
4✔
145
        self.depscript_override = False
4✔
146
        self.depgen_disabled = False
4✔
147
        self.patch_fuzz_override = False
4✔
148
        self.indent_spaces = 0
4✔
149
        self.indent_tabs = 0
4✔
150
        self.section = {}
4✔
151
        self.declarative = False
4✔
152

4✔
153
        self.current_section = 'package'
4✔
154
        # None == main package
155
        self.current_package = None
4✔
156
        self.package_noarch = {}
157

4✔
158
    def reset(self):
4✔
159
        self._spec_file = None
160
        self._spec_name = None
4✔
161
        self._default_state()
4✔
162

4✔
163
    def check_source(self, pkg):
4✔
164
        """Find specfile in SRPM and run spec file related checks."""
165
        wrong_spec = False
4✔
166
        self._spec_file = None
167
        self._spec_name = None
4✔
168

4✔
169
        # Check if a specfile exist in a specified path
4✔
170
        for fname, pkgfile in pkg.files.items():
171
            if fname.endswith('.spec'):
172
                self._spec_file = pkgfile.path
4✔
173
                self._spec_name = pkgfile.name
4✔
174
                if fname == pkg.name + '.spec':
4✔
175
                    wrong_spec = False
4✔
176
                    break
4✔
177
                else:
4✔
178
                    wrong_spec = True
4✔
179

180
        # method call
4✔
181
        self._check_no_spec_file(pkg)
182
        self._check_invalid_spec_name(pkg, wrong_spec)
183

4✔
184
        if self._spec_file:
4✔
185
            # check content of spec file
186
            with Pkg.FakePkg(self._spec_file) as package:
4✔
187
                self.check_spec(package)
188

4✔
189
    def check_spec(self, pkg):
4✔
190
        """Find specfile in specified path and run spec file related checks."""
191
        self._spec_file = pkg.name
4✔
192
        self._spec_file_dir = str(Path(self._spec_file).parent)
193

4✔
194
        # method call
4✔
195
        self._check_non_utf8_spec_file(pkg)
196

197
        self.pkg = pkg
4✔
198
        self.spec_only = isinstance(pkg, Pkg.FakePkg)
199

4✔
200
        spec_lines = readlines(self._spec_file)
4✔
201
        # Analyse specfile line by line to check for (E)rrors or (W)arnings
202
        # And initialize the SpecCheck instance for following checks
4✔
203
        self._check_lines(spec_lines)
204

205
        # Run checks for whole package
4✔
206
        self._check_no_buildroot_tag(pkg, self.buildroot)
207

208
        if not self.declarative:
4✔
209
            self._check_no_s_section(pkg, self.section)
210

4✔
211
        self._check_superfluous_clean_section(pkg, self.section)
4✔
212
        self._check_more_than_one_changelog_section(pkg, self.section)
213
        self._check_lib_package_without_mklibname(pkg, self.is_lib_pkg, self.mklibname)
4✔
214
        self._check_descript_without_disabling_depgen(pkg, self.depscript_override,
4✔
215
                                                      self.depgen_disabled)
4✔
216
        self._check_patch_fuzz_is_changed(pkg, self.patch_fuzz_override)
4✔
217
        self._check_mixed_use_of_space_and_tabs(pkg, self.indent_spaces, self.indent_tabs)
218
        self.check_ifarch_and_not_applied_patches(pkg, self.patches_auto_applied, self.patches,
4✔
219
                                                  self.applied_patches_ifarch, self.applied_patches)
4✔
220

4✔
221
        # Checks below require a real spec file
222
        if not self._spec_file:
223
            return
224
        self._check_specfile_error(pkg)
4✔
225
        self._check_invalid_url(pkg, rpm)
×
226

4✔
227
    def _check_no_spec_file(self, pkg):
4✔
228
        """Check if no spec file is found in RPM meta data."""
4✔
229
        if not self._spec_file:
230
            self.output.add_info('E', pkg, 'no-spec-file')
4✔
231

232
    def _check_invalid_spec_name(self, pkg, wrong_spec):
4✔
233
        """Check if spec file has same name as the 'Name: ' tag."""
4✔
234
        if wrong_spec and self._spec_file:
235
            self.output.add_info('E', pkg, 'invalid-spec-name')
4✔
236

237
    def _check_non_utf8_spec_file(self, pkg):
4✔
238
        """Check if spec file has UTF-8 character encoding."""
4✔
239
        if self._spec_file and not Pkg.is_utf8(self._spec_file):
240
            self.output.add_info('E', pkg, 'non-utf8-spec-file',
4✔
241
                                 self._spec_name or self._spec_file)
242

4✔
243
    def _check_no_buildroot_tag(self, pkg, buildroot):
4✔
244
        """Check if BuildRoot tag is used in the specfile."""
245
        if not buildroot:
246
            self.output.add_info('W', pkg, 'no-buildroot-tag')
4✔
247

248
    def _check_no_s_section(self, pkg, section):
4✔
249
        """Check if there is no (%prep, %build, %install, %check)
4✔
250
        in the specfile.
251
        """
4✔
252
        for sec in ('prep', 'build', 'install', 'check'):
253
            if not section.get(sec):
254
                self.output.add_info('W', pkg, 'no-%%%s-section' % sec)
255

4✔
256
    def _check_superfluous_clean_section(self, pkg, section):
4✔
257
        """Check for a superfluous %clean section in the specfile.
4✔
258
        """
259
        if section.get('clean'):
4✔
260
            self.output.add_info('E', pkg, 'superfluous-%clean-section')
261

262
    def _check_more_than_one_changelog_section(self, pkg, section):
4✔
263
        """Check if specfile has more than one %changelog.
4✔
264
        prep, build, install, check prevented by rpmbuild 4.4
265
        """
4✔
266
        if section.get('changelog', 0) > 1:
267
            self.output.add_info('W', pkg, 'more-than-one-%changelog-section')
268

269
    def _check_lib_package_without_mklibname(self, pkg, is_lib_pkg, mklibname):
4✔
270
        """Check if package name is built using %mklibname to allow lib64 and lib32
4✔
271
        coexistence. This check is specific to Mandriva and it's derivatives,
272
        check issue #9 in rpm-software-management/rpmlint/issues
4✔
273
        """
274
        if is_lib_pkg and not mklibname:
275
            self.output.add_info('E', pkg, 'lib-package-without-%mklibname')
276

277
    def _check_descript_without_disabling_depgen(self, pkg, depscript_override, depgen_disabled):
4✔
278
        """Check if specfile has %define _use_internal_dependency_generator set to 0
4✔
279
        to disable it, or does not have define __find_provides/requires.
280
        """
4✔
281
        if depscript_override and not depgen_disabled:
282
            self.output.add_info('W', pkg, 'depscript-without-disabling-depgen')
283

284
    def _check_patch_fuzz_is_changed(self, pkg, patch_fuzz_override):
4✔
285
        """Check if specfile has internal patch fuzz was changed."""
4✔
286
        if patch_fuzz_override:
287
            self.output.add_info('W', pkg, 'patch-fuzz-is-changed')
4✔
288

289
    def _check_mixed_use_of_space_and_tabs(self, pkg, indent_spaces, indent_tabs):
4✔
290
        """Check if specfile has mixed uses of spaces and tabs."""
4✔
291
        if indent_spaces and indent_tabs:
292
            pkg.current_linenum = max(indent_spaces, indent_tabs)
4✔
293
            self.output.add_info('W', pkg, 'mixed-use-of-spaces-and-tabs',
294
                                 '(spaces: line %d, tab: line %d)' %
4✔
295
                                 (indent_spaces, indent_tabs))
4✔
296
            pkg.current_linenum = None
4✔
297

298
    def check_ifarch_and_not_applied_patches(self, pkg, patches_auto_applied,
299
                                             patches, applied_patches_ifarch, applied_patches):
4✔
300
        """Check if specfile has a patch applied inside an %ifarch block.
301
        and check if a patch was included but not applied."""
4✔
302
        if not patches_auto_applied:
303
            for pnum, pfile in patches.items():
304
                if pnum in applied_patches_ifarch:
305
                    self.output.add_info('W', pkg, '%ifarch-applied-patch',
4✔
306
                                         'Patch%d:' % pnum, pfile)
4✔
307

4✔
308
                # Check if a patch is included in specfile but was not applied.
4✔
309
                if pnum not in applied_patches:
310
                    self.output.add_info('W', pkg, 'patch-not-applied',
311
                                         'Patch%d:' % pnum, pfile)
312

4✔
313
    def _check_specfile_error(self, pkg):
4✔
314
        """It parse the specfile with rpm and forward errors to rpmlint output."""
315

316
        # We'd like to parse the specfile only once using python bindings,
4✔
317
        # but it seems errors from rpmlib get logged to stderr and we can't
318
        # capture and print them nicely, so we do it once each way :P
319
        try:
320
            outcmd = subprocess.run(
321
                ('rpm', '-q', '--qf=', '-D', '_sourcedir %s' % self._spec_file_dir,
322
                 '--specfile', self._spec_file), stderr=subprocess.PIPE, encoding='utf8', env=ENGLISH_ENVIRONMENT)
4✔
323

4✔
324
            for line in outcmd.stderr.splitlines():
325
                line = line.strip()
326
                if not line:
327
                    continue
4✔
328
                if line and 'warning:' not in line:
4✔
329
                    self.output.add_info('E', pkg, 'specfile-error', line)
4✔
330
                else:
4✔
331
                    self.output.add_info('W', pkg, 'specfile-warning', line)
4✔
332
        except UnicodeDecodeError as e:
4✔
333
            self.output.add_info('E', pkg, 'specfile-error', str(e))
334

4✔
335
    def _check_invalid_url(self, pkg, rpm):
4✔
336
        """Check if specfile has an invalid url."""
4✔
337
        # grab sources and patches from parsed spec object to get
338
        # them with macros expanded for URL checking
4✔
339
        spec_obj = None
340
        rpm.addMacro('_sourcedir', self._spec_file_dir)
341
        try:
342
            transaction_set = rpm.TransactionSet()
4✔
343
            spec_obj = transaction_set.parseSpec(str(self._spec_file))
4✔
344
        except (ValueError, rpm.error) as e:
4✔
345
            self.output.add_info('E', pkg, 'specfile-error', str(e).strip(),
4✔
346
                                 str(self._spec_file))
4✔
347
        rpm.delMacro('_sourcedir')
4✔
348
        if spec_obj:
4✔
349
            for src in spec_obj.sources:
350
                (url, num, flags) = src
4✔
351
                (scheme, netloc) = urlparse(url)[0:2]
4✔
352
                if flags & 1:  # rpmspec.h, rpm.org ticket #123
4✔
353
                    srctype = 'Source'
4✔
354
                else:
4✔
355
                    srctype = 'Patch'
4✔
356
                tag = f'{srctype}{num}'
4✔
357
                if scheme and netloc:
358
                    continue
4✔
359
                elif srctype == 'Source' and tarball_regex.search(url):
4✔
360
                    self.output.add_info('W', pkg, 'invalid-url', '%s:' % tag, url)
4✔
361

4✔
362
    def _check_lines(self, lines):
4✔
363
        # gather info from spec lines
4✔
364
        self.pkg.current_linenum = 0
365
        for line in lines:
4✔
366
            self.pkg.current_linenum += 1
367
            self._check_line(line)
4✔
368
        # Last line read is not useful after this point
4✔
369
        self.pkg.current_linenum = None
4✔
370

4✔
371
    def _check_line(self, line):
372
        """
4✔
373
        Run check methods for this line.
374
        """
4✔
375

376
        self._checkline_declarative(line)
377
        self._checkline_break_space(line)
378
        if self._checkline_section(line):
379
            return
4✔
380
        self._checkline_buildroot_usage(line)
4✔
381
        self._checkline_make_check(line)
4✔
382
        self._checkline_setup(line)
4✔
383
        self._checkline_autopatch(line)
4✔
384
        self._checkline_applied_patch(line)
4✔
385
        self._checkline_sourcedir(line)
4✔
386
        self._checkline_configure(line)
4✔
387
        self._checkline_hardcoded_library_path(line)
4✔
388
        self._checkline_mklibname(line)
4✔
389
        self._checkline_package(line)
4✔
390
        self._checkline_changelog(line)
4✔
391
        self._checkline_files(line)
4✔
392
        self._checkline_indent(line)
4✔
393
        self._checkline_deprecated_grep(line)
4✔
394
        self._checkline_valid_groups(line)
4✔
395
        self._checkline_macros_in_comments(line)
4✔
396
        self._checkline_python_setup_test(line)
4✔
397
        self._checkline_python_setup_install(line)
4✔
398
        self._checkline_python_module_def(line)
4✔
399
        self._checkline_python_sitelib_glob(line)
4✔
400
        self._checkline_shared_dir_glob(line)
4✔
401

4✔
402
        # If statement, starts
4✔
403
        if ifarch_regex.search(line):
4✔
404
            self.if_depth = self.if_depth + 1
405
            self.ifarch_depth = self.if_depth
406
        elif if_regex.search(line):
4✔
407
            self.if_depth = self.if_depth + 1
4✔
408

4✔
409
        # If statement, ends
4✔
410
        elif endif_regex.search(line):
4✔
411
            if self.ifarch_depth == self.if_depth:
412
                self.ifarch_depth = -1
413
            self.if_depth = self.if_depth - 1
4✔
414

4✔
415
    # line checks methods
4✔
416

4✔
417
    def _checkline_declarative(self, line):
418
        # Do not override if we found the regex in previous lines
419
        if self.declarative:
420
            return
4✔
421
        self.declarative = bool(declarative_regex.search(line))
422
        if self.declarative:
4✔
423
            # we assume implicit %prep
4✔
424
            self.patches_auto_applied = True
4✔
425

4✔
426
    def _checkline_break_space(self, line):
427
        char = line.find(UNICODE_NBSP)
4✔
428
        if char != -1:
429
            self.output.add_info('W', self.pkg, 'non-break-space', 'line %s, char %d' %
4✔
430
                                 (self.pkg.current_linenum, char))
4✔
431

4✔
432
    def _checkline_section(self, line):
4✔
433
        section_marker = False
434
        for sec, regex in section_regexs.items():
435
            res = regex.search(line)
4✔
436
            if res:
4✔
437
                self.current_section = sec
4✔
438
                section_marker = True
4✔
439
                self.section[sec] = self.section.get(sec, 0) + 1
4✔
440
                if sec in ('package', 'files'):
4✔
441
                    rest = filelist_regex.sub('', line[res.end() - 1:])
4✔
442
                    res = pkgname_regex.search(rest)
4✔
443
                    if res:
4✔
444
                        self.current_package = res.group(1)
4✔
445
                    else:
4✔
446
                        self.current_package = None
4✔
447
                break
4✔
448

449
        if section_marker:
4✔
450
            if not self.is_lib_pkg and lib_package_regex.search(line):
4✔
451
                self.is_lib_pkg = True
452
            return True
4✔
453

4✔
454
    def _checkline_buildroot_usage(self, line):
4✔
455
        if (self.current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and
4✔
456
                contains_buildroot(line)):
457
            self.output.add_info('E', self.pkg, 'rpm-buildroot-usage', '%' + self.current_section,
4✔
458
                                 line[:-1].strip())
4✔
459

460
    def _checkline_make_check(self, line):
4✔
461
        if make_check_regex.search(line) and self.current_section not in \
462
                ('check', 'changelog', 'package', 'description'):
463
            self.output.add_info('W', self.pkg, 'make-check-outside-check-section',
4✔
464
                                 line[:-1])
4✔
465

466
    def _checkline_setup(self, line):
4✔
467
        # %setup check
468
        if setup_regex.match(line):
469
            if not setup_q_regex.search(line):
4✔
470
                # Don't warn if there's a -T without -a or -b
471
                if setup_t_regex.search(line):
4✔
472
                    if setup_ab_regex.search(line):
4✔
473
                        self.output.add_info('W', self.pkg, 'setup-not-quiet')
474
                else:
4✔
475
                    self.output.add_info('W', self.pkg, 'setup-not-quiet')
×
476

×
477
            if self.current_section != 'prep':
478
                self.output.add_info('W', self.pkg, 'setup-not-in-prep')
4✔
479
            return
480

4✔
481
        res = autosetup_regex.search(line)
4✔
482
        if res:
4✔
483
            if not autosetup_n_regex.search(res.group(1)):
484
                self.patches_auto_applied = True
4✔
485
            if self.current_section != 'prep':
4✔
486
                self.output.add_info('W', self.pkg, '%autosetup-not-in-prep')
4✔
487

4✔
488
    def _checkline_autopatch(self, line):
4✔
489
        # %autopach check
4✔
490
        if autopatch_regex.search(line):
491
            self.patches_auto_applied = True
4✔
492
            if self.current_section != 'prep':
493
                self.output.add_info('W', self.pkg, '%autopatch-not-in-prep')
4✔
494

4✔
495
    def _checkline_applied_patch(self, line):
4✔
496
        # Check for %patch -P
4✔
497
        res = applied_patch_regex.search(line)
498
        if res:
4✔
499
            # Check for %patchN (not supported by rpm >= 4.20)
500
            if applied_patch_rpm420_regex.match(line):
4✔
501
                self.output.add_info('E', self.pkg, 'patch-macro-old-format')
4✔
502

503
            pnum = res.group(1) or 0
4✔
504
            for tmp in applied_patch_p_regex.findall(line) or [pnum]:
4✔
505
                pnum = int(tmp)
506
                self.applied_patches.append(pnum)
4✔
507
                if self.ifarch_depth > 0:
4✔
508
                    self.applied_patches_ifarch.append(pnum)
4✔
509
            return
4✔
510

4✔
511
        # Check for %{PATCH0} | patch
4✔
512
        res = applied_patch_pipe_regex.search(line)
4✔
513
        if res:
514
            pnum = int(res.group(1))
515
            self.applied_patches.append(pnum)
4✔
516
            if self.ifarch_depth > 0:
4✔
517
                self.applied_patches_ifarch.append(pnum)
4✔
518
            return
4✔
519

4✔
520
        # Check for patch < %{PATCH0}
×
521
        res = applied_patch_i_regex.search(line)
4✔
522
        if res:
523
            pnum = int(res.group(1))
524
            self.applied_patches.append(pnum)
4✔
525
            if self.ifarch_depth > 0:
4✔
526
                self.applied_patches_ifarch.append(pnum)
4✔
527
            return
4✔
528

4✔
529
    def _checkline_sourcedir(self, line):
×
530
        if self.source_dir:
4✔
531
            return
532

4✔
533
        res = source_dir_regex.search(line)
4✔
534
        if res:
4✔
535
            self.source_dir = True
536
            self.output.add_info('E', self.pkg, 'use-of-RPM_SOURCE_DIR')
4✔
537

4✔
538
    def _checkline_configure(self, line):
4✔
539
        if self.configure_linenum:
4✔
540
            if self.configure_cmdline[-1] == '\\':
541
                self.configure_cmdline = self.configure_cmdline[:-1] + line.strip()
4✔
542
            else:
4✔
543
                res = configure_libdir_spec_regex.search(self.configure_cmdline)
4✔
544
                if not res:
4✔
545
                    # Hack to get the correct (start of ./configure) line
546
                    # number displayed:
4✔
547
                    real_linenum = self.pkg.current_linenum
4✔
548
                    self.pkg.current_linenum = self.configure_linenum
549
                    self.output.add_info('W', self.pkg, 'configure-without-libdir-spec')
550
                    self.pkg.current_linenum = real_linenum
4✔
551
                elif res.group(1):
4✔
552
                    res = re.match(hardcoded_library_paths, res.group(1))
4✔
553
                    if res:
4✔
554
                        self.output.add_info('E', self.pkg, 'hardcoded-library-path',
4✔
555
                                             res.group(1), 'in configure options')
4✔
556
                self.configure_linenum = None
4✔
557

×
558
        hash_pos = line.find('#')
559

4✔
560
        if self.current_section != 'changelog':
561
            cfg_pos = line.find('./configure')
4✔
562
            if cfg_pos != -1 and (hash_pos == -1 or hash_pos > cfg_pos):
563
                # store line where it started
4✔
564
                self.configure_linenum = self.pkg.current_linenum
4✔
565
                self.configure_cmdline = line.strip()
4✔
566

567
    def _checkline_hardcoded_library_path(self, line):
4✔
568
        res = hardcoded_library_path_regex.search(line)
4✔
569
        if self.current_section != 'changelog' and res and not \
570
                (biarch_package_regex.match(self.pkg.name) or
4✔
571
                 self.hardcoded_lib_path_exceptions_regex.search(
4✔
572
                     res.group(1).lstrip())):
4✔
573
            self.output.add_info('E', self.pkg, 'hardcoded-library-path', 'in',
574
                                 res.group(1).lstrip())
575

576
    def _checkline_mklibname(self, line):
4✔
577
        self.mklibname = '%mklibname' in line
578

579
    # line checks package methods
4✔
580
    def _checkline_package_patch(self, line):
4✔
581
        # Would be cleaner to get sources and patches from the
582
        # specfile parsed in Python (see below), but we want to
583
        # catch %ifarch'd etc ones as well, and also catch these when
4✔
584
        # the specfile is not parseable.
585
        res = patch_regex.search(line)
586
        if res:
587
            pnum = int(res.group(1) or 0)
588
            self.patches[pnum] = res.group(2)
4✔
589

4✔
590
    def _checkline_package_obsolete_tags(self, line):
4✔
591
        res = obsolete_tags_regex.search(line)
4✔
592
        if res:
593
            self.output.add_info('W', self.pkg, 'obsolete-tag', res.group(1))
4✔
594

4✔
595
    def _checkline_package_buildroot(self, line):
4✔
596
        res = buildroot_regex.search(line)
4✔
597
        if res:
598
            self.buildroot = True
4✔
599
            if res.group(1).startswith('/'):
4✔
600
                self.output.add_info('W', self.pkg, 'hardcoded-path-in-buildroot-tag',
4✔
601
                                     res.group(1))
4✔
602

4✔
603
    def _checkline_package_buildarch(self, line):
4✔
604
        res = buildarch_regex.search(line)
605
        if res:
606
            if res.group(1) != 'noarch':
4✔
607
                self.output.add_info('E', self.pkg,
4✔
608
                                     'buildarch-instead-of-exclusivearch-tag',
4✔
609
                                     res.group(1))
4✔
610
            else:
4✔
611
                self.package_noarch[self.current_package] = True
612

613
    def _checkline_package_packager(self, line):
614
        res = packager_regex.search(line)
4✔
615
        if res:
616
            self.output.add_info('W', self.pkg, 'hardcoded-packager-tag', res.group(1))
4✔
617

4✔
618
    def _checkline_package_prefix(self, line):
4✔
619
        res = prefix_regex.search(line)
4✔
620
        if res and not res.group(1).startswith('%'):
621
            self.output.add_info('W', self.pkg, 'hardcoded-prefix-tag', res.group(1))
4✔
622

4✔
623
    def _checkline_package_prereq(self, line):
4✔
624
        res = prereq_regex.search(line)
4✔
625
        if res:
626
            self.output.add_info('E', self.pkg, 'prereq-use', res.group(2))
4✔
627

4✔
628
    def _checkline_package_buildprereq(self, line):
4✔
629
        res = buildprereq_regex.search(line)
4✔
630
        if res:
4✔
631
            self.output.add_info('E', self.pkg, 'buildprereq-use', res.group(1))
4✔
632

4✔
633
    def _checkline_package_requires(self, line):
4✔
634
        res = requires_regex.search(line)
635
        if res:
4✔
636
            reqs = Pkg.parse_deps(res.group(1))
4✔
637
            deptoken = Pkg.has_forbidden_controlchars(reqs)
4✔
638
            if deptoken:
4✔
639
                self.output.add_info('E', self.pkg,
640
                                     'forbidden-controlchar-found',
4✔
641
                                     f'Requires: {deptoken}')
4✔
642
            for req in unversioned(reqs):
4✔
643
                if compop_regex.search(req):
4✔
644
                    self.output.add_info('W', self.pkg,
645
                                         'comparison-operator-in-deptoken',
4✔
646
                                         req)
4✔
647

4✔
648
    def _checkline_package_provides(self, line):
4✔
649
        res = provides_regex.search(line)
4✔
650
        if res:
4✔
651
            provs = Pkg.parse_deps(res.group(1))
4✔
652
            deptoken = Pkg.has_forbidden_controlchars(provs)
653
            if deptoken:
654
                self.output.add_info('E', self.pkg,
4✔
655
                                     'forbidden-controlchar-found',
4✔
656
                                     f'Provides: {deptoken}')
4✔
657
            for prov in unversioned(provs):
658
                if not prov.startswith('/'):
659
                    self.output.add_info('W', self.pkg, 'unversioned-explicit-provides',
660
                                         prov)
4✔
661
                if compop_regex.search(prov):
4✔
662
                    self.output.add_info('W', self.pkg,
4✔
663
                                         'comparison-operator-in-deptoken',
4✔
664
                                         prov)
4✔
665

4✔
666
    def _checkline_package_obsoletes(self, line):
4✔
667
        res = obsoletes_regex.search(line)
668
        if res:
669
            obses = Pkg.parse_deps(res.group(1))
4✔
670
            deptoken = Pkg.has_forbidden_controlchars(obses)
4✔
671
            if deptoken:
4✔
672
                self.output.add_info('E', self.pkg,
673
                                     'forbidden-controlchar-found',
4✔
674
                                     f'Obsoletes: {deptoken}')
4✔
675
            for obs in unversioned(obses):
676
                if not obs.startswith('/'):
677
                    self.output.add_info('W', self.pkg, 'unversioned-explicit-obsoletes',
678
                                         obs)
4✔
679
                if compop_regex.search(obs):
4✔
680
                    self.output.add_info('W', self.pkg,
4✔
681
                                         'comparison-operator-in-deptoken',
4✔
682
                                         obs)
4✔
683

4✔
684
    def _checkline_package_conflicts(self, line):
4✔
685
        res = conflicts_regex.search(line)
686
        if res:
687
            confs = Pkg.parse_deps(res.group(1))
4✔
688
            deptoken = Pkg.has_forbidden_controlchars(confs)
4✔
689
            if deptoken:
4✔
690
                self.output.add_info('E', self.pkg,
691
                                     'forbidden-controlchar-found',
4✔
692
                                     f'Conflicts: {deptoken}')
×
693
            for conf in unversioned(confs):
694
                if compop_regex.search(conf):
695
                    self.output.add_info('W', self.pkg,
696
                                         'comparison-operator-in-deptoken',
4✔
697
                                         conf)
4✔
698

4✔
699
    def _checkline_package(self, line):
4✔
700
        if self.current_section != 'package':
4✔
701
            return
4✔
702

4✔
703
        self._checkline_package_patch(line)
704
        self._checkline_package_obsolete_tags(line)
705
        self._checkline_package_buildroot(line)
4✔
706
        self._checkline_package_buildarch(line)
4✔
707
        self._checkline_package_packager(line)
4✔
708
        self._checkline_package_prefix(line)
709
        self._checkline_package_prereq(line)
710
        self._checkline_package_buildprereq(line)
711
        self._checkline_package_requires(line)
4✔
712
        self._checkline_package_provides(line)
4✔
713
        self._checkline_package_obsoletes(line)
4✔
714
        self._checkline_package_conflicts(line)
715

4✔
716
        self._checkline_forbidden_controlchars(line)
4✔
717
        self._check_suse_update_desktop_file(line)
4✔
718

4✔
719
    def _checkline_changelog(self, line):
4✔
720
        if self.current_section == 'changelog':
4✔
721
            deptoken = Pkg.has_forbidden_controlchars(line)
4✔
722
            if deptoken:
4✔
723
                self.output.add_info('E', self.pkg,
4✔
724
                                     'forbidden-controlchar-found',
4✔
725
                                     '%%changelog: %s' % deptoken)
4✔
726
            for match in self.macro_regex.findall(line):
4✔
727
                res = re.match('%+', match)
4✔
728
                if len(res.group(0)) % 2 and match != '%autochangelog' and match != '%{autochangelog}':
729
                    self.output.add_info('W', self.pkg, 'macro-in-%changelog', match)
4✔
730
        else:
4✔
731
            if not self.depscript_override:
732
                self.depscript_override = \
4✔
733
                    depscript_override_regex.search(line) is not None
4✔
734
            if not self.depgen_disabled:
4✔
735
                self.depgen_disabled = \
4✔
736
                    depgen_disable_regex.search(line) is not None
4✔
737
            if not self.patch_fuzz_override:
738
                self.patch_fuzz_override = \
739
                    patch_fuzz_override_regex.search(line) is not None
4✔
740

4✔
741
    def _checkline_files(self, line):
4✔
742
        # TODO: check scriptlets for these too?
4✔
743
        if (self.current_section == 'files' and
744
                (self.package_noarch.get(self.current_package) or
4✔
745
                    (self.current_package not in self.package_noarch and self.package_noarch.get(None)))):
4✔
746
            res = libdir_regex.search(line)
747
            if res:
4✔
748
                pkgname = self.current_package
4✔
749
                if pkgname is None:
750
                    pkgname = '(main package)'
4✔
751
                self.output.add_info('W', self.pkg, 'libdir-macro-in-noarch-package',
4✔
752
                                     pkgname, line.rstrip())
753

754
    def _checkline_indent(self, line):
4✔
755
        if not self.indent_tabs and '\t' in line:
756
            self.indent_tabs = self.pkg.current_linenum
4✔
757
        if not self.indent_spaces and indent_spaces_regex.search(line):
758
            self.indent_spaces = self.pkg.current_linenum
759

4✔
760
    def _checkline_deprecated_grep(self, line):
4✔
761
        # Check if egrep or fgrep is used
4✔
762
        if self.current_section not in \
4✔
763
                ('package', 'changelog', 'description', 'files'):
4✔
764
            greps = deprecated_grep_regex.findall(line)
4✔
765
            if greps:
766
                self.output.add_info('W', self.pkg, 'deprecated-grep', greps)
767

4✔
768
    def _checkline_valid_groups(self, line):
4✔
769
        # If not checking spec file only, we're checking one inside a
4✔
770
        # SRPM -> skip this check to avoid duplicate warnings (#167)
4✔
771

4✔
772
        if self.spec_only and self.valid_groups and \
773
           line.lower().startswith('group:'):
4✔
774
            group = line[6:].strip()
775
            if group not in self.valid_groups:
4✔
776
                self.output.add_info('W', self.pkg, 'non-standard-group', group)
777

4✔
778
    def _checkline_macros_in_comments(self, line):
4✔
779
        hash_pos = line.find('#')
4✔
780
        # Test if there are macros in comments
781
        if hash_pos != -1 and \
4✔
782
                (hash_pos == 0 or line[hash_pos - 1] in (' ', '\t')):
783

784
            comment = line[hash_pos + 1:]
785
            # Ignore special comments like #!BuildIgnore
4✔
786
            if comment and comment[0] == '!':
787
                return
×
788

×
789
            for match in self.macro_regex.findall(comment):
×
790
                res = re.match('%+', match)
791
                if len(res.group(0)) % 2:
4✔
792
                    self.output.add_info('W', self.pkg, 'macro-in-comment', match)
4✔
793

794
    def _checkline_python_setup_test(self, line):
4✔
795
        # Test if the "python setup.py test" deprecated subcommand is used
796
        if self.current_section == 'check' and python_setup_test_regex.search(line):
797
            self.output.add_info('W', self.pkg, 'python-setup-test', line[:-1])
4✔
798

799
    def _checkline_python_setup_install(self, line):
4✔
800
        # Test if the "python setup.py install" deprecated subcommand is used
4✔
801
        if self.current_section == 'install' and python_setup_install_regex.search(line):
802
            self.output.add_info('W', self.pkg, 'python-setup-install', line[:-1])
4✔
803

4✔
804
    def _checkline_python_module_def(self, line):
4✔
805
        """
4✔
806
        Test if the "python_module" macro is defined in the spec file
807
        This macro was in py2pack but now it should be provided by
4✔
808
        python-rpm-macros
809
        """
4✔
810
        if python_module_def_regex.search(line):
4✔
811
            self.output.add_info('W', self.pkg, 'python-module-def', line[:-1])
812

4✔
813
    def _checkline_python_sitelib_glob(self, line):
814
        """Test if %{python_sitelib}/* is present in %files section."""
4✔
815
        if self.current_section != 'files':
4✔
816
            return
817

4✔
818
        if python_sitelib_glob_regex.match(line):
819
            self.output.add_info('W', self.pkg, 'python-sitelib-glob-in-files',
820
                                 line[:-1])
821

822
    def _checkline_shared_dir_glob(self, line):
823
        """Test if %{_bindir}/*, etc. is present in %files section."""
4✔
824
        if self.current_section != 'files':
4✔
825
            return
826

4✔
827
        if shared_dir_glob_regex.match(line):
828
            self.output.add_info('W', self.pkg, 'shared-dir-glob-in-files',
4✔
829
                                 line[:-1])
4✔
830

831
    def _checkline_forbidden_controlchars(self, line):
4✔
832
        """Look for controlchar in any line"""
4✔
833
        # https://github.com/rpm-software-management/rpmlint/issues/1067
834
        if Pkg.has_forbidden_controlchars(line):
835
            self.output.add_info('W', self.pkg, 'forbidden-controlchar-found')
4✔
836

837
    def _check_suse_update_desktop_file(self, line):
4✔
838
        """
4✔
839
        Test if update-desktop-files is in BuildRequires. The usage of
840
        %suse_update_desktop_file is deprecated now.
4✔
841
        """
4✔
842
        if suse_update_desktop_file_regex.match(line):
843
            # Don't show the message for yast, there's no migration path yet.
844
            if 'yast' in self.pkg.name.lower():
4✔
845
                return
846

847
            self.output.add_info('W', self.pkg,
4✔
848
                                 'suse-update-desktop-file-deprecated',
4✔
849
                                 '%suse_update_desktop_file is deprecated')
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc