rpmlint/SCLCheck.py

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.''',
)