forked from rpm-software-management/rpmlint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SCLCheck.py
417 lines (361 loc) · 15.9 KB
/
SCLCheck.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# -*- coding: utf-8 -*-
#############################################################################
# File : SCLCheck.py
# Package : rpmlint
# Author : Miro Hrončok
# Created on : Wed Jul 24 20:25 2013
# Purpose : Software Collections checks.
#############################################################################
import os
import re
import AbstractCheck
from Filter import addDetails, printError, printWarning
import Pkg
# Compile all regexes here
allowed_etc = re.compile(r'^/etc/(cron|profile|logrotate)\.d/', re.M)
allowed_var = re.compile(r'^/var/(log|lock)/', re.M)
buildrequires = re.compile(r'^BuildRequires:\s*(.*)', re.M)
global_scl_definition = re.compile(r'(^|\s)%(define|global)\s+scl\s+\S+\s*$', re.M)
libdir = re.compile(r'%\{?\??_libdir\}?', re.M)
name = re.compile(r'^Name:\s*(.*)', re.M)
name_small = re.compile(r'^%\{?name\}?', re.M)
noarch = re.compile(r'^BuildArch:\s*noarch\s*$', re.M)
obsoletes_conflicts = re.compile(r'^(Obsoletes|(Build)?Conflicts):\s*(.*)', re.M)
pkg_name = re.compile(r'(^|\s)%\{!\?scl:%(define|global)\s+pkg_name\s+%\{name\}\}\s*$', re.M)
provides = re.compile(r'^Provides:\s*(.*)', re.M)
requires = re.compile(r'(^|:)Requires:\s*(.*)', re.M)
scl_files = re.compile(r'(^|\s)%\{?\??scl_files\}?\s*$', re.M)
scl_install = re.compile(r'(^|\s)%\{?\??scl_install\}?\s*$', re.M)
scl_macros = re.compile(r'(^|\s)%\{?\??_root_sysconfdir\}?/rpm/macros\.%\{?\??scl\}?-config\s*^', re.M)
scl_package_definition = re.compile(r'(^|\s)%\{\?scl\s*:\s*%scl_package\s+\S+\s*\}\s*$', re.M)
scl_prefix_noncond = re.compile(r'%\{?scl_prefix\}?', re.M)
scl_prefix = re.compile(r'%\{?\??scl_prefix\}?', re.M)
scl_prefix_start = re.compile(r'^%\{?\??scl_prefix\}?', re.M)
scl_runtime = re.compile(r'%\{?\??scl\}?-runtime\}?', re.M)
scl_use = re.compile(r'%\{?\??\!?\??scl')
setup = re.compile(r'^%setup(.*)', re.M)
startdir = re.compile(r'^/opt/[^/]+/', re.M)
subpackage_alien = re.compile(r'(^|\s)%package\s+(-n\s+)?(?!(build|runtime))\S+\s*$', re.M)
subpackage_any = re.compile(r'(^|\s)%package\s+(.*)', re.M)
subpackage_build = re.compile(r'(^|\s)%package\s+build\s*$', re.M)
subpackage_runtime = re.compile(r'(^|\s)%package\s+runtime\s*$', re.M)
def index_or_sub(source, word, sub=0):
"""
Helper function that returns index of word in source or sub when not found.
"""
try:
return source.index(word)
except:
return sub
class SCLCheck(AbstractCheck.AbstractCheck):
'''Software Collections checks'''
def __init__(self):
AbstractCheck.AbstractCheck.__init__(self, "SCLCheck")
self._spec_file = None
def check_source(self, pkg):
# lookup spec file
for fname, pkgfile in pkg.files().items():
if fname.endswith('.spec'):
self._spec_file = pkgfile.path
self.check_spec(pkg, self._spec_file)
def check_spec(self, pkg, spec_file, spec_lines=[]):
'''SCL spec file checks'''
spec = '\n'.join(Pkg.readlines(spec_file))
if global_scl_definition.search(spec):
self.check_metapackage(pkg, spec)
elif scl_package_definition.search(spec):
self.check_scl_spec(pkg, spec)
elif scl_use.search(spec):
printError(pkg, 'undeclared-scl')
def check_binary(self, pkg):
'''SCL binary package checks'''
# Assume that no dash in package name means no SCL
splits = pkg.name.split('-')
if len(splits) < 2:
return
scl_name = splits[0]
# While we are here, check if it's a runtime/build package
is_runtime = splits[-1] == 'runtime'
is_build = splits[-1] == 'build'
del splits
# Now test if there is /opt/foo/ dir
good = False
for fname in pkg.files().keys():
if startdir.search(fname):
good = True
break
if not good:
return
# Test if our dir is named the same way as scl
good = True
for fname in pkg.files().keys():
if not startdir.search(fname):
if allowed_etc.search(fname) or allowed_var.search(fname) or \
fname.startswith('/usr/bin/'):
continue
if fname.startswith('/etc/rpm/'):
if not is_build:
printWarning(pkg, 'scl-rpm-macros-outside-of-build',
fname)
continue
if is_runtime and \
fname == os.path.join('/etc/scl/prefixes', scl_name):
continue
printError(pkg, 'file-outside-of-scl-tree', fname)
else:
if fname.split('/')[3] != scl_name:
good = False
if not good:
printError(pkg, 'scl-name-screwed-up')
def check_metapackage(self, pkg, spec):
'''SCL metapackage spec checks'''
# Examine subpackages
runtime = subpackage_runtime.search(spec)
if not runtime:
printError(pkg, 'no-runtime-in-scl-metapackage')
build = subpackage_build.search(spec)
if not build:
printError(pkg, 'no-build-in-scl-metapackage')
else:
# Get (B)Rs section for build subpackage
end = index_or_sub(spec[build.end():], '%package', -1)
if 'scl-utils-build' not in \
' '.join(self.get_requires(spec[build.end():end])):
printWarning(pkg,
'scl-build-without-requiring-scl-utils-build')
alien = subpackage_alien.search(spec)
if alien:
printError(pkg, 'weird-subpackage-in-scl-metapackage',
alien.group()[9:])
# Get (B)Rs section for main package
end = index_or_sub(spec, '%package', -1)
if 'scl-utils-build' not in \
' '.join(self.get_build_requires(spec[:end])):
printError(pkg, 'scl-metapackage-without-scl-utils-build-br')
# Enter %install section
install_start = index_or_sub(spec, '%install')
install_end = index_or_sub(spec, '%check')
if not install_end:
install_end = index_or_sub(spec, '%clean')
if not install_end:
install_end = index_or_sub(spec, '%files')
if not install_end:
install_end = index_or_sub(spec, '%changelog', -1)
# Search %scl_install
if not scl_install.search(spec[install_start:install_end]):
printError(pkg, 'scl-metapackage-without-%scl_install')
if noarch.search(spec[:install_start]) and \
libdir.search(spec[install_start:install_end]):
printError(pkg, 'noarch-scl-metapackage-with-libdir')
# Analyze %files
files = self.get_files(spec)
if files:
printWarning(pkg, 'scl-main-metapackage-contains-files',
', '.join(files))
if runtime:
if not scl_files.search(
'\n'.join(self.get_files(spec, 'runtime'))):
printError(pkg, 'scl-runtime-package-without-%scl_files')
if build:
if not scl_macros.search(
'\n'.join(self.get_files(spec, 'build'))):
printError(pkg, 'scl-build-package-without-rpm-macros')
def check_scl_spec(self, pkg, spec):
'''SCL ready spec checks'''
# For the entire spec
if not pkg_name.search(spec):
printWarning(pkg, 'missing-pkg_name-definition')
if scl_prefix_noncond.search(self.remove_scl_conds(spec)):
printWarning(pkg, 'scl-prefix-without-condition')
if not scl_prefix.search(self.get_name(spec)):
printError(pkg, 'name-without-scl-prefix')
for item in self.get_obsoletes_and_conflicts(spec):
if not scl_prefix.search(item):
printError(pkg, 'obsoletes-or-conflicts-without-scl-prefix')
break
for item in self.get_provides(spec):
if not scl_prefix.search(item):
printError(pkg, 'provides-without-scl-prefix')
break
setup_opts = setup.search(spec)
if setup_opts:
if '-n' not in setup_opts.groups()[0]:
printError(pkg, 'scl-setup-without-n')
# Examine main package and subpackages one by one
borders = []
borders.append(0) # main package starts at the beginning
while True:
more = subpackage_any.search(spec[borders[-1]:])
if not more:
break
splits = more.groups()[1].split()
if len(splits) > 1 and splits[0] == '-n':
if not scl_prefix_start.search(splits[-1]):
printError(pkg, 'subpackage-with-n-without-scl-prefix')
# current end is counted only from last one
borders.append(borders[-1] + more.end())
subpackages = [(borders[i], borders[i + 1])
for i in range(len(borders) - 1)]
for subpackage in subpackages:
ok = False
for require in self.get_requires(spec[subpackage[0]:subpackage[1]]):
# Remove flase entries
if not require or require == ':':
continue
# If it starts with %{name}, it,s fine
# If it starts with SCL prefix, it's fine
# If it is scl-runtime, it's the best
if name_small.search(require) or \
scl_prefix_start.search(require) or \
scl_runtime.match(require):
ok = True
break
if not ok:
printError(pkg,
'doesnt-require-scl-runtime-or-other-scl-package')
break
def get_requires(self, text, build=False):
'''For given piece of spec, find Requires (or BuildRequires)'''
if build:
search = buildrequires
else:
search = requires
res = []
while True:
more = search.search(text)
if not more:
break
res.extend(more.groups())
text = text[more.end():]
return res
def get_build_requires(self, text):
'''Call get_requires() with build = True'''
return self.get_requires(text, True)
def get_name(self, text):
'''For given piece of spec, get the Name of the main package'''
sname = name.search(text)
if not sname:
return None
return sname.groups()[0].strip()
def get_obsoletes_and_conflicts(self, text):
'''For given piece of spec, find Obsoletes and Conflicts'''
res = []
while True:
more = obsoletes_conflicts.search(text)
if not more:
break
# 1st group is 'Obsoletes' or 'Conflicts', 2nd is Build or None
res.extend(more.groups()[2:])
text = text[more.end():]
return res
def get_provides(self, text):
'''For given piece of spec, find Provides'''
res = []
while True:
more = provides.search(text)
if not more:
break
res.extend(more.groups())
text = text[more.end():]
return res
def get_files(self, text, subpackage=None):
"""
Return the list of files in %files section for given subpackage or
main package.
"""
if subpackage:
pattern = r'%%\{?\??files\}?(\s+-n)?\s+%s\s*$' % subpackage
else:
pattern = r'%\{?\??files\}?\s*$'
search = re.search(pattern, text, re.M)
if not search:
return []
start = search.end()
end = index_or_sub(text[start:], '%files')
if not end:
end = index_or_sub(text[start:], '%changelog', -1)
return list(filter(None, text[start:start + end].strip().split('\n')))
def remove_scl_conds(self, text):
'''Returns given text without %scl conds blocks'''
while text.count('%{?scl:') > 0:
spos = text.index('%{?scl:')
pos = spos + 7
counter = 1
while counter:
if text[pos] == '{':
counter += 1
if text[pos] == '}':
counter -= 1
pos += 1
text = text[:spos] + text[pos:]
return text
# Create an object to enable the auto registration of the test
check = SCLCheck()
# Add information about checks
addDetails(
'undeclared-scl',
'''Specfile contains %scl* macros, but was not recognized as SCL metapackage or
SCL ready package. If this should be an SCL metapackage, don't forget to define
the %scl macro. If this should be an SCL ready package, run %scl
conditionalized %scl_package macro, e.g. %{?scl:%scl_package foo}.''',
'no-runtime-in-scl-metapackage',
'SCL metapackage must have runtime subpackage.',
'no-build-in-scl-metapackage',
'SCL metapackage must have build subpackage.',
'weird-subpackage-in-scl-metapackage',
'Only allowed subpackages in SCL metapackage are build and runtime.',
'scl-metapackage-without-scl-utils-build-br',
'SCL metapackage must BuildRequire scl-utils-build.',
'scl-build-without-requiring-scl-utils-build',
'SCL runtime package should Require scl-utils-build.',
'scl-metapackage-without-%scl_install',
'SCL metapackage must call %scl_install in the %install section.',
'noarch-scl-metapackage-with-libdir',
'''If "enable" script of SCL metapackage contains %{_libdir}, the package must
be arch specific, otherwise it may be noarch.''',
'scl-main-metapackage-contains-files',
'Main package of SCL metapackage should not contain any files.',
'scl-runtime-package-without-%scl_files',
'SCL runtime package must contain %scl_files in %files section.',
'scl-build-package-without-rpm-macros',
'''SCL build package must contain %{_root_sysconfdir}/rpm/macros. %{scl}-config
in %files section.''',
'missing-pkg_name-definition',
'%{!?scl:%global pkg_name %{name}} is missing in the specfile.',
'name-without-scl-prefix',
'Name of SCL package must start with %{?scl_prefix}.',
'scl-prefix-without-condition',
'''The SCL prefix is used without condition - this won't work if the package is
build outside of SCL - use %{?scl_prefix} with questionmark.''',
'obsoletes-or-conflicts-without-scl-prefix',
'''Obsoletes, Conflicts and Build Conflicts must always be prefixed with
%{?scl_prefix}. This is extremely important, as the SCLs are often used for
deploying new packages on older systems (that may contain old packages, now
obsoleted by the new ones), but they shouldn't Obsolete or Conflict with the
non-SCL RPMs installed on the system (that's the idea of SCL).''',
'provides-without-scl-prefix',
'Provides tag must always be prefixed with %{?scl_prefix}.',
'doesnt-require-scl-runtime-or-other-scl-package',
'''The package must require %{scl}-runtime, unless it depends on another
package that requires %{scl}-runtime. It's impossible to check what other
packages require, so this simply checks if this package requires at least
something from its collection.''',
'subpackage-with-n-without-scl-prefix',
'''If (and only if) a package defines its name with -n, the name must be
prefixed with %{?scl_prefix}.''',
'scl-setup-without-n',
'''The %setup macro needs the -n argument for SCL builds, because the directory
with source probably doesn't include SCL prefix in its name.''',
'scl-name-screwed-up',
'''SCL package's name starts with SCL prefix. That prefix is used as a
directory, where files are stored: If the prefix is foo, the directory is
/opt/provides/foo. This package doesn't respect that. This means either the
name of the package is wrong, or the directory.''',
'file-outside-of-scl-tree',
'''SCL package should only contain files in /opt/provider/scl-name directory or
in other allowed directories such as some directories in /etc or /var. Wrapper
scripts in /usr/bin are also allowed.''',
'scl-rpm-macros-outside-of-build',
'''RPM macros in SCL packages should belong to -build subpackage of the SCL
metapackage.''',
)