• 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

82.69
/rpmlint/lint.py
1
from collections import defaultdict
4✔
2
import cProfile
4✔
3
import importlib
4✔
4
import operator
4✔
5
from pstats import Stats
4✔
6
import sys
4✔
7
from tempfile import gettempdir
4✔
8
import time
4✔
9

4✔
10
from rpmlint.color import Color
4✔
11
from rpmlint.config import Config
4✔
12
from rpmlint.filter import Filter
13
from rpmlint.helpers import print_warning, string_center
4✔
14
from rpmlint.pkg import FakePkg, get_installed_pkgs, Pkg
4✔
15
from rpmlint.version import __version__
4✔
16

4✔
17

4✔
18
class Lint:
4✔
19
    """
20
    Generic object handling the basic rpmlint operations
21
    """
4✔
22

23
    def __init__(self, options):
24
        # initialize configuration
25
        self.checks = {}
26
        self.options = options
4✔
27
        self.packages_checked = 0
28
        self.specfiles_checked = 0
4✔
29
        self.check_duration = defaultdict(int)
30
        if options['config']:
4✔
31
            self.config = Config(options['config'])
4✔
32
        else:
4✔
33
            self.config = Config()
4✔
34
        if options['profile']:
4✔
35
            self.profile = cProfile.Profile()
4✔
36
            self.profile.enable()
4✔
37
        else:
38
            self.profile = None
4✔
39

4✔
40
        self._load_rpmlintrc()
×
41
        if options['verbose']:
×
42
            self.config.info = options['verbose']
43
        if options['strict']:
4✔
44
            self.config.strict = options['strict']
45
        if options['permissive']:
4✔
46
            self.config.permissive = options['permissive']
4✔
47
        if not self.config.configuration['ExtractDir']:
2✔
48
            self.config.configuration['ExtractDir'] = gettempdir()
4✔
49
        # initialize output buffer
4✔
50
        self.output = Filter(self.config)
4✔
51
        # preload the check list if we not print config
2✔
52
        # some of the config values are transformed e.g. to regular
4✔
53
        # expressions
×
54
        if not self.options['print_config']:
55
            self.load_checks()
4✔
56

4✔
57
    def _run(self):
58
        start = time.monotonic()
4✔
59
        retcode = 0
60
        # if we just want to print config, do so and leave
61
        if self.options['print_config']:
62
            self.print_config()
63
            return retcode
4✔
64
        # just explain the error and abort too
4✔
65
        if self.options['explain']:
×
66
            self.print_explanation(self.options['explain'], self.config)
×
67
            return retcode
68

69
        # if there are installed arguments just load them up as extra
70
        # items to the rpmfile option
71
        if self.options['installed']:
4✔
72
            self.validate_installed_packages(self._load_installed_rpms(self.options['installed']))
4✔
73
        # if no exclusive option is passed then just loop over all the
74
        # arguments that are supposed to be either rpm or spec files
4✔
75
        self.validate_files(self.options['rpmfile'])
4✔
76
        self._print_header()
4✔
77
        print(self.output.print_results(self.output.results, self.config),
78
              end='')
4✔
79
        quit_color = Color.Bold
4✔
80
        if self.output.printed_messages['W'] > 0:
4✔
81
            quit_color = Color.Yellow
82
        if self.output.badness_threshold > 0 and self.output.score > self.output.badness_threshold:
4✔
83
            msg = string_center(f'Badness {self.output.score} exceeds threshold {self.output.badness_threshold}, aborting.', '-')
4✔
84
            print(f'{Color.Red}{msg}{Color.Reset}')
4✔
85
            quit_color = Color.Red
86
            retcode = 66
87
        elif self.output.printed_messages['E'] > 0 and not self.config.permissive:
88
            quit_color = Color.Red
4✔
89
            all_promoted = self.output.printed_messages['E'] == self.output.promoted_to_error
2✔
90
            retcode = 65 if all_promoted else 64
91

92
        self._maybe_print_reports()
4✔
93

4✔
94
        duration = time.monotonic() - start
4✔
95
        error_messages = self.output.printed_messages['E']
96
        warning_messages = self.output.printed_messages['W']
4✔
97
        msg = string_center(f'{self.packages_checked} packages and {self.specfiles_checked} specfiles checked; '
4✔
98
                            f'{error_messages} errors, {warning_messages} warnings'
4✔
99
                            f', {self.output.filtered_out} filtered, '
4✔
100
                            f'{self.output.score} badness; has taken {duration:.1f} s', '=')
×
101
        print(f'{quit_color}{msg}{Color.Reset}')
×
102

×
103
        return retcode
×
104

4✔
105
    def run(self):
4✔
106
        try:
4✔
107
            return self._run()
4✔
108
        except KeyboardInterrupt as e:
109
            self._maybe_print_reports()
4✔
110
            raise e
111

4✔
112
    def _maybe_print_reports(self):
4✔
113
        if self.options['time_report']:
4✔
114
            self._print_time_report()
4✔
115
        if self.profile:
116
            self._print_cprofile()
117

118
    def _get_color_time_report_value(self, fraction):
4✔
119
        if fraction > 25:
120
            color = Color.Red
4✔
121
        elif fraction > 5:
122
            color = Color.Yellow
4✔
123
        else:
4✔
124
            color = ''
4✔
125
        return f'{color}{fraction:17.1f}{Color.Reset}'
×
126

×
127
    def _print_time_report(self):
×
128
        PERCENT_THRESHOLD = 1
129
        TIME_THRESHOLD = 0.1
4✔
130
        total = sum(self.check_duration.values())
4✔
131
        checked_files = [check.checked_files for check in self.checks.values() if check.checked_files]
4✔
132
        total_checked_files = max(checked_files) if checked_files else ''
4✔
133
        print(f'{Color.Bold}Check time report{Color.Reset} (>{PERCENT_THRESHOLD}% & >{TIME_THRESHOLD}s):')
×
134

135
        check = format('Check', '32s')
4✔
136
        duration = format('Duration (in s)', '>12')
×
137
        fraction = format('Fraction (in %)', '>17')
×
138
        print(f'{Color.Bold}    {check} {duration} {fraction}  Checked files{Color.Reset}')
×
139

×
140
        for check, duration in sorted(self.check_duration.items(), key=operator.itemgetter(1), reverse=True):
141
            fraction = 100.0 * duration / total
×
142
            if fraction < PERCENT_THRESHOLD or duration < TIME_THRESHOLD:
×
143
                continue
144

4✔
145
            checked_files = ''
4✔
146
            if check in self.checks:
4✔
147
                checked = self.checks[check].checked_files
4✔
148
                if checked:
4✔
149
                    checked_files = checked
4✔
150
            print(f'    {check:32s} {duration:15.1f} {self._get_color_time_report_value(fraction)} {checked_files:>14}')
4✔
151

152
        print(f'    {"TOTAL":32s} {total:15.1f} {100:17.1f} {total_checked_files:>14}\n')       # noqa Q000
4✔
153

4✔
154
    def _print_cprofile(self):
4✔
155
        N = 30
4✔
156
        print(f'{Color.Bold}cProfile report:{Color.Reset}')
157
        self.profile.disable()
4✔
158
        stats = Stats(self.profile)
×
159
        stats.sort_stats('cumulative').print_stats(N)
×
160
        print('========================================================')
×
161
        stats.sort_stats('ncalls').print_stats(N)
162
        print('========================================================')
×
163
        stats.sort_stats('tottime').print_stats(N)
×
164

×
165
    def _load_installed_rpms(self, packages):
×
166
        existing_packages = []
×
167
        for name in packages:
×
168
            pkg = get_installed_pkgs(name)
169
            if pkg:
4✔
170
                existing_packages.extend(pkg)
171
            else:
4✔
172
                print_warning(f'(none): E: there is no installed rpm "{name}".')
×
173
        return existing_packages
×
174

×
175
    def _load_rpmlintrc(self):
×
176
        """
×
177
        Load rpmlintrc from argument or load up from folder
×
178
        """
×
179
        if self.options['rpmlintrc']:
×
180
            for rcfile in self.options['rpmlintrc']:
×
181
                self.config.load_rpmlintrc(rcfile)
182
        else:
4✔
183
            # load only from the same folder specname.rpmlintrc or specname-rpmlintrc
2✔
184
            # do this only in a case where there is one folder parameter or one file
2✔
185
            # to avoid multiple folders handling
2✔
186
            rpmlintrc = []
2✔
187
            if len(self.options['rpmfile']) != 1:
2✔
188
                return
189
            pkg = self.options['rpmfile'][0]
2✔
190
            if pkg.is_file():
2✔
191
                pkg = pkg.parent
192
            rpmlintrc += sorted(pkg.glob('*.rpmlintrc'))
4✔
193
            rpmlintrc += sorted(pkg.glob('*-rpmlintrc'))
4✔
194
            if len(rpmlintrc) > 1:
4✔
195
                # multiple rpmlintrcs are highly undesirable
4✔
196
                print_warning('There are multiple items to be loaded for rpmlintrc, ignoring them: {}.'.format(' '.join(map(str, rpmlintrc))))
4✔
197
            elif len(rpmlintrc) == 1:
198
                self.options['rpmlintrc'] = rpmlintrc[0]
4✔
199
                self.config.load_rpmlintrc(rpmlintrc[0])
200

201
    def _print_header(self):
202
        """
4✔
203
        Print out header information about the state of the
4✔
204
        rpmlint prior printing out the check report.
205
        """
4✔
206
        intro = string_center('rpmlint session starts', '=')
207
        print(f'{Color.Bold}{intro}{Color.Reset}')
×
208
        print(f'rpmlint: {__version__}')
×
209
        print('configuration:')
4✔
210
        for config in self.config.conf_files:
211
            print(f'    {config}')
212
        if self.options['rpmlintrc']:
213
            rpmlintrc = self.options['rpmlintrc']
4✔
214
            print(f'rpmlintrc: {rpmlintrc}')
4✔
215
        no_checks = len(self.config.configuration['Checks'])
4✔
216
        no_pkgs = len(self.options['installed']) + len(self.options['rpmfile'])
4✔
217
        print(f'{Color.Bold}checks: {no_checks}, packages: {no_pkgs}{Color.Reset}')
218
        print('')
4✔
219

220
    def validate_installed_packages(self, packages):
2✔
221
        # Do not run post checks if there are also plain rpm/spec files to validate
222
        run_post_checks = not bool(self.options['rpmfile'])
4✔
223
        for pkg in packages:
2✔
224
            self.run_checks(pkg, run_post_checks and pkg == packages[-1])
2✔
225
            self.reset_checks()
226

4✔
227
    def validate_files(self, files):
228
        """
229
        Run all the check for passed file list
230
        """
231
        if not files:
4✔
232
            if self.packages_checked == 0:
4✔
233
                # print warning only if we didn't process even installed files
4✔
234
                print_warning('There are no files to process nor additional arguments.')
4✔
235
                print_warning('Nothing to do, aborting.')
4✔
236
            return
4✔
237
        # check all elements if they are a folder or a file with proper suffix
4✔
238
        # and expand everything
2✔
239
        packages = self._expand_filelist(files)
2✔
240

2✔
241
        # Sort the files so that the output is stable
4✔
242
        packages = sorted(packages)
4✔
243
        for pkg in packages:
4✔
244
            self.validate_file(pkg, pkg == packages[-1])
4✔
245
            self.reset_checks()
246

4✔
247
    def _expand_filelist(self, files):
248
        packages = []
2✔
249
        for pkg in files:
2✔
250
            if pkg.is_file() and pkg.suffix in ('.rpm', '.spm', '.spec'):
2✔
251
                packages.append(pkg)
2✔
252
            elif pkg.is_dir():
253
                packages.extend(self._expand_filelist(pkg.iterdir()))
4✔
254
        return packages
255

256
    def validate_file(self, pname, is_last):
257
        try:
4✔
258
            if pname.suffix in ('.rpm', '.spm'):
4✔
259
                with Pkg(pname, self.config.configuration['ExtractDir'],
260
                         verbose=self.config.info) as pkg:
4✔
261
                    for k, v in pkg.timers.items():
4✔
262
                        self.check_duration[k] += v
4✔
263
                    self.run_checks(pkg, is_last)
264
            elif pname.suffix == '.spec':
265
                with FakePkg(pname) as pkg:
4✔
266
                    self.run_checks(pkg, is_last)
267
        except Exception as e:
268
            print_warning(f'(none): E: fatal error while reading {pname}: {e}')
4✔
269
            if self.config.info:
4✔
270
                raise e
4✔
271
            sys.exit(3)
4✔
272

273
    def run_checks(self, pkg, is_last):
4✔
274
        spec_checks = isinstance(pkg, FakePkg)
4✔
275
        for checker in self.checks:
4✔
276
            start = time.monotonic()
4✔
277
            fn = self.checks[checker].check_spec if spec_checks else self.checks[checker].check
4✔
278
            fn(pkg)
2✔
279
            self.check_duration[checker] += time.monotonic() - start
2✔
280

4✔
281
        # run post check function and validate used filters in rpmlintrc
282
        if is_last:
4✔
283
            for checker in self.checks.values():
4✔
284
                checker.after_checks()
4✔
285

4✔
286
            if not self.options['ignore_unused_rpmlintrc']:
287
                self.output.validate_filters(pkg)
4✔
288

4✔
289
        if spec_checks:
4✔
290
            self.specfiles_checked += 1
4✔
291
        else:
4✔
292
            self.packages_checked += 1
4✔
293

×
294
    def print_config(self):
×
295
        """
×
296
        Just output the current configuration
×
297
        """
×
298
        self.config.print_config()
299

4✔
300
    def print_explanation(self, messages, config):
4✔
301
        """
4✔
302
        Print out detailed explanation for the specified messages
4✔
303
        """
4✔
304
        for message in messages:
4✔
305
            explanation = self.output.get_description(message, config)
4✔
306
            if not explanation:
307
                # check if it's a WarnOnFunction warning configuration
308
                forbidden_functions = config.configuration['WarnOnFunction']
4✔
309
                if message in forbidden_functions:
4✔
310
                    explanation = forbidden_functions[message].get('description')
4✔
311

312
            if not explanation:
4✔
313
                explanation = 'Unknown message, please report a bug if the description should be present.\n\n'
4✔
314

315
            print(f'{message}:\n{explanation}')
4✔
316

4✔
317
    def load_checks(self):
318
        """
4✔
319
        Load all checks based on the config, skipping those already loaded
320
        SingletonTM
4✔
321
        """
322

323
        selected_checks = self.options['checks']
324
        if selected_checks:
4✔
325
            selected_checks = selected_checks.split(',')
326

4✔
327
        for check in self.config.configuration['Checks']:
328
            if check in self.checks:
329
                continue
330
            if not selected_checks or check in selected_checks:
4✔
331
                self.checks[check] = self.load_check(check)
4✔
332

4✔
333
    def reset_checks(self):
334
        """
4✔
335
        Reset all check objects to set to the default state
4✔
336
        """
4✔
337
        for check in self.checks.values():
338
            check.reset()
4✔
339

4✔
340
    def load_check(self, name):
341
        """Load a (check) module by its name, unless it is already loaded."""
4✔
342
        module = importlib.import_module(f'.{name}', package='rpmlint.checks')
343
        klass = getattr(module, name)
4✔
344
        obj = klass(self.config, self.output)
345
        return obj
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