418 lines
16 KiB
Python
418 lines
16 KiB
Python
# -*- 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 ValueError:
|
|
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=None):
|
|
'''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.''',
|
|
)
|