blob: 4ab23d1d6a0348746fb32962beb9ee4891a7f282 [file] [log] [blame]
# repoman: Checks
# Copyright 2007-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
"""This module contains functions used in Repoman to ascertain the quality
and correctness of an ebuild."""
from __future__ import unicode_literals
import codecs
from itertools import chain
import re
import time
import repoman.errors as errors
import portage
from portage.eapi import eapi_supports_prefix, eapi_has_implicit_rdepend, \
eapi_has_src_prepare_and_src_configure, eapi_has_dosed_dohard, \
eapi_exports_AA, eapi_has_pkg_pretend
class LineCheck(object):
"""Run a check on a line of an ebuild."""
"""A regular expression to determine whether to ignore the line"""
ignore_line = False
"""True if lines containing nothing more than comments with optional
leading whitespace should be ignored"""
ignore_comment = True
def new(self, pkg):
pass
def check_eapi(self, eapi):
""" returns if the check should be run in the given EAPI (default is True) """
return True
def check(self, num, line):
"""Run the check on line and return error if there is one"""
if self.re.match(line):
return self.error
def end(self):
pass
class PhaseCheck(LineCheck):
""" basic class for function detection """
func_end_re = re.compile(r'^\}$')
phases_re = re.compile('(%s)' % '|'.join((
'pkg_pretend', 'pkg_setup', 'src_unpack', 'src_prepare',
'src_configure', 'src_compile', 'src_test', 'src_install',
'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm',
'pkg_config')))
in_phase = ''
def check(self, num, line):
m = self.phases_re.match(line)
if m is not None:
self.in_phase = m.group(1)
if self.in_phase != '' and \
self.func_end_re.match(line) is not None:
self.in_phase = ''
return self.phase_check(num, line)
def phase_check(self, num, line):
""" override this function for your checks """
pass
class EbuildHeader(LineCheck):
"""Ensure ebuilds have proper headers
Copyright header errors
CVS header errors
License header errors
Args:
modification_year - Year the ebuild was last modified
"""
repoman_check_name = 'ebuild.badheader'
gentoo_copyright = r'^# Copyright ((1999|2\d\d\d)-)?%s Gentoo Foundation$'
# Why a regex here, use a string match
# gentoo_license = re.compile(r'^# Distributed under the terms of the GNU General Public License v2$')
gentoo_license = '# Distributed under the terms of the GNU General Public License v2'
id_header = '# $Id$'
ignore_comment = False
def new(self, pkg):
if pkg.mtime is None:
self.modification_year = r'2\d\d\d'
else:
self.modification_year = str(time.gmtime(pkg.mtime)[0])
self.gentoo_copyright_re = re.compile(
self.gentoo_copyright % self.modification_year)
def check(self, num, line):
if num > 2:
return
elif num == 0:
if not self.gentoo_copyright_re.match(line):
return errors.COPYRIGHT_ERROR
elif num == 1 and line.rstrip('\n') != self.gentoo_license:
return errors.LICENSE_ERROR
elif num == 2 and line.rstrip('\n') != self.id_header:
return errors.ID_HEADER_ERROR
class EbuildWhitespace(LineCheck):
"""Ensure ebuilds have proper whitespacing"""
repoman_check_name = 'ebuild.minorsyn'
ignore_line = re.compile(r'(^$)|(^(\t)*#)')
ignore_comment = False
leading_spaces = re.compile(r'^[\S\t]')
trailing_whitespace = re.compile(r'.*([\S]$)')
def check(self, num, line):
if self.leading_spaces.match(line) is None:
return errors.LEADING_SPACES_ERROR
if self.trailing_whitespace.match(line) is None:
return errors.TRAILING_WHITESPACE_ERROR
class EbuildBlankLine(LineCheck):
repoman_check_name = 'ebuild.minorsyn'
ignore_comment = False
blank_line = re.compile(r'^$')
def new(self, pkg):
self.line_is_blank = False
def check(self, num, line):
if self.line_is_blank and self.blank_line.match(line):
return 'Useless blank line on line: %d'
if self.blank_line.match(line):
self.line_is_blank = True
else:
self.line_is_blank = False
def end(self):
if self.line_is_blank:
yield 'Useless blank line on last line'
class EbuildQuote(LineCheck):
"""Ensure ebuilds have valid quoting around things like D,FILESDIR, etc..."""
repoman_check_name = 'ebuild.minorsyn'
_message_commands = ["die", "echo", "eerror",
"einfo", "elog", "eqawarn", "ewarn"]
_message_re = re.compile(r'\s(' + "|".join(_message_commands) + \
r')\s+"[^"]*"\s*$')
_ignored_commands = ["local", "export"] + _message_commands
ignore_line = re.compile(r'(^$)|(^\s*#.*)|(^\s*\w+=.*)' + \
r'|(^\s*(' + "|".join(_ignored_commands) + r')\s+)')
ignore_comment = False
var_names = ["D", "DISTDIR", "FILESDIR", "S", "T", "ROOT", "WORKDIR"]
# EAPI=3/Prefix vars
var_names += ["ED", "EPREFIX", "EROOT"]
# variables for games.eclass
var_names += ["Ddir", "GAMES_PREFIX_OPT", "GAMES_DATADIR",
"GAMES_DATADIR_BASE", "GAMES_SYSCONFDIR", "GAMES_STATEDIR",
"GAMES_LOGDIR", "GAMES_BINDIR"]
# variables for multibuild.eclass
var_names += ["BUILD_DIR"]
var_names = "(%s)" % "|".join(var_names)
var_reference = re.compile(r'\$(\{'+var_names+'\}|' + \
var_names + '\W)')
missing_quotes = re.compile(r'(\s|^)[^"\'\s]*\$\{?' + var_names + \
r'\}?[^"\'\s]*(\s|$)')
cond_begin = re.compile(r'(^|\s+)\[\[($|\\$|\s+)')
cond_end = re.compile(r'(^|\s+)\]\]($|\\$|\s+)')
def check(self, num, line):
if self.var_reference.search(line) is None:
return
# There can be multiple matches / violations on a single line. We
# have to make sure none of the matches are violators. Once we've
# found one violator, any remaining matches on the same line can
# be ignored.
pos = 0
while pos <= len(line) - 1:
missing_quotes = self.missing_quotes.search(line, pos)
if not missing_quotes:
break
# If the last character of the previous match is a whitespace
# character, that character may be needed for the next
# missing_quotes match, so search overlaps by 1 character.
group = missing_quotes.group()
pos = missing_quotes.end() - 1
# Filter out some false positives that can
# get through the missing_quotes regex.
if self.var_reference.search(group) is None:
continue
# Filter matches that appear to be an
# argument to a message command.
# For example: false || ewarn "foo $WORKDIR/bar baz"
message_match = self._message_re.search(line)
if message_match is not None and \
message_match.start() < pos and \
message_match.end() > pos:
break
# This is an attempt to avoid false positives without getting
# too complex, while possibly allowing some (hopefully
# unlikely) violations to slip through. We just assume
# everything is correct if the there is a ' [[ ' or a ' ]] '
# anywhere in the whole line (possibly continued over one
# line).
if self.cond_begin.search(line) is not None:
continue
if self.cond_end.search(line) is not None:
continue
# Any remaining matches on the same line can be ignored.
return errors.MISSING_QUOTES_ERROR
class EbuildAssignment(LineCheck):
"""Ensure ebuilds don't assign to readonly variables."""
repoman_check_name = 'variable.readonly'
readonly_assignment = re.compile(r'^\s*(export\s+)?(A|CATEGORY|P|PV|PN|PR|PVR|PF|D|WORKDIR|FILESDIR|FEATURES|USE)=')
def check(self, num, line):
match = self.readonly_assignment.match(line)
e = None
if match is not None:
e = errors.READONLY_ASSIGNMENT_ERROR
return e
class Eapi3EbuildAssignment(EbuildAssignment):
"""Ensure ebuilds don't assign to readonly EAPI 3-introduced variables."""
readonly_assignment = re.compile(r'\s*(export\s+)?(ED|EPREFIX|EROOT)=')
def check_eapi(self, eapi):
return eapi_supports_prefix(eapi)
class EbuildNestedDie(LineCheck):
"""Check ebuild for nested die statements (die statements in subshells)"""
repoman_check_name = 'ebuild.nesteddie'
nesteddie_re = re.compile(r'^[^#]*\s\(\s[^)]*\bdie\b')
def check(self, num, line):
if self.nesteddie_re.match(line):
return errors.NESTED_DIE_ERROR
class EbuildUselessDodoc(LineCheck):
"""Check ebuild for useless files in dodoc arguments."""
repoman_check_name = 'ebuild.minorsyn'
uselessdodoc_re = re.compile(
r'^\s*dodoc(\s+|\s+.*\s+)(ABOUT-NLS|COPYING|LICENCE|LICENSE)($|\s)')
def check(self, num, line):
match = self.uselessdodoc_re.match(line)
if match:
return "Useless dodoc '%s'" % (match.group(2), ) + " on line: %d"
class EbuildUselessCdS(LineCheck):
"""Check for redundant cd ${S} statements"""
repoman_check_name = 'ebuild.minorsyn'
method_re = re.compile(r'^\s*src_(prepare|configure|compile|install|test)\s*\(\)')
cds_re = re.compile(r'^\s*cd\s+("\$(\{S\}|S)"|\$(\{S\}|S))\s')
def __init__(self):
self.check_next_line = False
def check(self, num, line):
if self.check_next_line:
self.check_next_line = False
if self.cds_re.match(line):
return errors.REDUNDANT_CD_S_ERROR
elif self.method_re.match(line):
self.check_next_line = True
class EapiDefinition(LineCheck):
"""
Check that EAPI assignment conforms to PMS section 7.3.1
(first non-comment, non-blank line).
"""
repoman_check_name = 'EAPI.definition'
ignore_comment = True
_eapi_re = portage._pms_eapi_re
def new(self, pkg):
self._cached_eapi = pkg.eapi
self._parsed_eapi = None
self._eapi_line_num = None
def check(self, num, line):
if self._eapi_line_num is None and line.strip():
self._eapi_line_num = num + 1
m = self._eapi_re.match(line)
if m is not None:
self._parsed_eapi = m.group(2)
def end(self):
if self._parsed_eapi is None:
if self._cached_eapi != "0":
yield "valid EAPI assignment must occur on or before line: %s" % \
self._eapi_line_num
elif self._parsed_eapi != self._cached_eapi:
yield ("bash returned EAPI '%s' which does not match "
"assignment on line: %s") % \
(self._cached_eapi, self._eapi_line_num)
class EbuildPatches(LineCheck):
"""Ensure ebuilds use bash arrays for PATCHES to ensure white space safety"""
repoman_check_name = 'ebuild.patches'
re = re.compile(r'^\s*PATCHES=[^\(]')
error = errors.PATCHES_ERROR
class EbuildQuotedA(LineCheck):
"""Ensure ebuilds have no quoting around ${A}"""
repoman_check_name = 'ebuild.minorsyn'
a_quoted = re.compile(r'.*\"\$(\{A\}|A)\"')
def check(self, num, line):
match = self.a_quoted.match(line)
if match:
return "Quoted \"${A}\" on line: %d"
class NoOffsetWithHelpers(LineCheck):
""" Check that the image location, the alternate root offset, and the
offset prefix (D, ROOT, ED, EROOT and EPREFIX) are not used with
helpers """
repoman_check_name = 'variable.usedwithhelpers'
# Ignore matches in quoted strings like this:
# elog "installed into ${ROOT}usr/share/php5/apc/."
re = re.compile(r'^[^#"\']*\b(docinto|docompress|dodir|dohard|exeinto|fowners|fperms|insinto|into)\s+"?\$\{?(D|ROOT|ED|EROOT|EPREFIX)\b.*')
error = errors.NO_OFFSET_WITH_HELPERS
class ImplicitRuntimeDeps(LineCheck):
"""
Detect the case where DEPEND is set and RDEPEND is unset in the ebuild,
since this triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4).
"""
repoman_check_name = 'RDEPEND.implicit'
_assignment_re = re.compile(r'^\s*(R?DEPEND)\+?=')
def new(self, pkg):
self._rdepend = False
self._depend = False
def check_eapi(self, eapi):
# Beginning with EAPI 4, there is no
# implicit RDEPEND=$DEPEND assignment
# to be concerned with.
return eapi_has_implicit_rdepend(eapi)
def check(self, num, line):
if not self._rdepend:
m = self._assignment_re.match(line)
if m is None:
pass
elif m.group(1) == "RDEPEND":
self._rdepend = True
elif m.group(1) == "DEPEND":
self._depend = True
def end(self):
if self._depend and not self._rdepend:
yield 'RDEPEND is not explicitly assigned'
class InheritDeprecated(LineCheck):
"""Check if ebuild directly or indirectly inherits a deprecated eclass."""
repoman_check_name = 'inherit.deprecated'
# deprecated eclass : new eclass (False if no new eclass)
deprecated_classes = {
"bash-completion": "bash-completion-r1",
"boost-utils": False,
"distutils": "distutils-r1",
"gems": "ruby-fakegem",
"mono": "mono-env",
"python": "python-r1 / python-single-r1 / python-any-r1",
"ruby": "ruby-ng",
"x-modular": "xorg-2",
}
_inherit_re = re.compile(r'^\s*inherit\s(.*)$')
def new(self, pkg):
self._errors = []
def check(self, num, line):
direct_inherits = None
m = self._inherit_re.match(line)
if m is not None:
direct_inherits = m.group(1)
if direct_inherits:
direct_inherits = direct_inherits.split()
if not direct_inherits:
return
for eclass in direct_inherits:
replacement = self.deprecated_classes.get(eclass)
if replacement is None:
pass
elif replacement is False:
self._errors.append("please migrate from " + \
"'%s' (no replacement) on line: %d" % (eclass, num + 1))
else:
self._errors.append("please migrate from " + \
"'%s' to '%s' on line: %d" % \
(eclass, replacement, num + 1))
def end(self):
for error in self._errors:
yield error
del self._errors
class InheritEclass(LineCheck):
"""
Base class for checking for missing inherits, as well as excess inherits.
Args:
eclass: Set to the name of your eclass.
funcs: A tuple of functions that this eclass provides.
comprehensive: Is the list of functions complete?
exempt_eclasses: If these eclasses are inherited, disable the missing
inherit check.
"""
def __init__(self, eclass, funcs=None, comprehensive=False,
exempt_eclasses=None, ignore_missing=False, **kwargs):
self._eclass = eclass
self._comprehensive = comprehensive
self._exempt_eclasses = exempt_eclasses
self._ignore_missing = ignore_missing
inherit_re = eclass
self._inherit_re = re.compile(r'^(\s*|.*[|&]\s*)\binherit\s(.*\s)?%s(\s|$)' % inherit_re)
# Match when the function is preceded only by leading whitespace, a
# shell operator such as (, {, |, ||, or &&, or optional variable
# setting(s). This prevents false positives in things like elog
# messages, as reported in bug #413285.
self._func_re = re.compile(r'(^|[|&{(])\s*(\w+=.*)?\b(' + '|'.join(funcs) + r')\b')
def new(self, pkg):
self.repoman_check_name = 'inherit.missing'
# We can't use pkg.inherited because that tells us all the eclasses that
# have been inherited and not just the ones we inherit directly.
self._inherit = False
self._func_call = False
if self._exempt_eclasses is not None:
inherited = pkg.inherited
self._disabled = any(x in inherited for x in self._exempt_eclasses)
else:
self._disabled = False
self._eapi = pkg.eapi
def check(self, num, line):
if not self._inherit:
self._inherit = self._inherit_re.match(line)
if not self._inherit:
if self._disabled or self._ignore_missing:
return
s = self._func_re.search(line)
if s is not None:
func_name = s.group(3)
eapi_func = _eclass_eapi_functions.get(func_name)
if eapi_func is None or not eapi_func(self._eapi):
self._func_call = True
return ('%s.eclass is not inherited, '
'but "%s" found at line: %s') % \
(self._eclass, func_name, '%d')
elif not self._func_call:
self._func_call = self._func_re.search(line)
def end(self):
if not self._disabled and self._comprehensive and self._inherit and not self._func_call:
self.repoman_check_name = 'inherit.unused'
yield 'no function called from %s.eclass; please drop' % self._eclass
_eclass_eapi_functions = {
"usex" : lambda eapi: eapi not in ("0", "1", "2", "3", "4", "4-python", "4-slot-abi")
}
# eclasses that export ${ECLASS}_src_(compile|configure|install)
_eclass_export_functions = (
'ant-tasks', 'apache-2', 'apache-module', 'aspell-dict',
'autotools-utils', 'base', 'bsdmk', 'cannadic',
'clutter', 'cmake-utils', 'db', 'distutils', 'elisp',
'embassy', 'emboss', 'emul-linux-x86', 'enlightenment',
'font-ebdftopcf', 'font', 'fox', 'freebsd', 'freedict',
'games', 'games-ggz', 'games-mods', 'gdesklets',
'gems', 'gkrellm-plugin', 'gnatbuild', 'gnat', 'gnome2',
'gnome-python-common', 'gnustep-base', 'go-mono', 'gpe',
'gst-plugins-bad', 'gst-plugins-base', 'gst-plugins-good',
'gst-plugins-ugly', 'gtk-sharp-module', 'haskell-cabal',
'horde', 'java-ant-2', 'java-pkg-2', 'java-pkg-simple',
'java-virtuals-2', 'kde4-base', 'kde4-meta', 'kernel-2',
'latex-package', 'linux-mod', 'mozlinguas', 'myspell',
'myspell-r2', 'mysql', 'mysql-v2', 'mythtv-plugins',
'oasis', 'obs-service', 'office-ext', 'perl-app',
'perl-module', 'php-ext-base-r1', 'php-ext-pecl-r2',
'php-ext-source-r2', 'php-lib-r1', 'php-pear-lib-r1',
'php-pear-r1', 'python-distutils-ng', 'python',
'qt4-build', 'qt4-r2', 'rox-0install', 'rox', 'ruby',
'ruby-ng', 'scsh', 'selinux-policy-2', 'sgml-catalog',
'stardict', 'sword-module', 'tetex-3', 'tetex',
'texlive-module', 'toolchain-binutils', 'toolchain',
'twisted', 'vdr-plugin-2', 'vdr-plugin', 'vim',
'vim-plugin', 'vim-spell', 'virtuoso', 'vmware',
'vmware-mod', 'waf-utils', 'webapp', 'xemacs-elisp',
'xemacs-packages', 'xfconf', 'x-modular', 'xorg-2',
'zproduct'
)
_eclass_info = {
'autotools': {
'funcs': (
'eaclocal', 'eautoconf', 'eautoheader',
'eautomake', 'eautoreconf', '_elibtoolize',
'eautopoint'
),
'comprehensive': True,
# Exempt eclasses:
# git - An EGIT_BOOTSTRAP variable may be used to call one of
# the autotools functions.
# subversion - An ESVN_BOOTSTRAP variable may be used to call one of
# the autotools functions.
'exempt_eclasses': ('git', 'git-2', 'subversion', 'autotools-utils')
},
'eutils': {
'funcs': (
'estack_push', 'estack_pop', 'eshopts_push', 'eshopts_pop',
'eumask_push', 'eumask_pop', 'epatch', 'epatch_user',
'emktemp', 'edos2unix', 'in_iuse', 'use_if_iuse', 'usex'
),
'comprehensive': False,
# These are "eclasses are the whole ebuild" type thing.
'exempt_eclasses': _eclass_export_functions,
},
'flag-o-matic': {
'funcs': (
'filter-(ld)?flags', 'strip-flags', 'strip-unsupported-flags',
'append-((ld|c(pp|xx)?))?flags', 'append-libs',
),
'comprehensive': False
},
'libtool': {
'funcs': (
'elibtoolize',
),
'comprehensive': True,
'exempt_eclasses': ('autotools',)
},
'multilib': {
'funcs': (
'get_libdir',
),
# These are "eclasses are the whole ebuild" type thing.
'exempt_eclasses': _eclass_export_functions + ('autotools', 'libtool',
'multilib-minimal'),
'comprehensive': False
},
'multiprocessing': {
'funcs': (
'makeopts_jobs',
),
'comprehensive': False
},
'prefix': {
'funcs': (
'eprefixify',
),
'comprehensive': True
},
'toolchain-funcs': {
'funcs': (
'gen_usr_ldscript',
),
'comprehensive': False
},
'user': {
'funcs': (
'enewuser', 'enewgroup',
'egetent', 'egethome', 'egetshell', 'esethome'
),
'comprehensive': True
}
}
class EMakeParallelDisabled(PhaseCheck):
"""Check for emake -j1 calls which disable parallelization."""
repoman_check_name = 'upstream.workaround'
re = re.compile(r'^\s*emake\s+.*-j\s*1\b')
error = errors.EMAKE_PARALLEL_DISABLED
def phase_check(self, num, line):
if self.in_phase == 'src_compile' or self.in_phase == 'src_install':
if self.re.match(line):
return self.error
class EMakeParallelDisabledViaMAKEOPTS(LineCheck):
"""Check for MAKEOPTS=-j1 that disables parallelization."""
repoman_check_name = 'upstream.workaround'
re = re.compile(r'^\s*MAKEOPTS=(\'|")?.*-j\s*1\b')
error = errors.EMAKE_PARALLEL_DISABLED_VIA_MAKEOPTS
class NoAsNeeded(LineCheck):
"""Check for calls to the no-as-needed function."""
repoman_check_name = 'upstream.workaround'
re = re.compile(r'.*\$\(no-as-needed\)')
error = errors.NO_AS_NEEDED
class PreserveOldLib(LineCheck):
"""Check for calls to the deprecated preserve_old_lib function."""
repoman_check_name = 'ebuild.minorsyn'
re = re.compile(r'.*preserve_old_lib')
error = errors.PRESERVE_OLD_LIB
class SandboxAddpredict(LineCheck):
"""Check for calls to the addpredict function."""
repoman_check_name = 'upstream.workaround'
re = re.compile(r'(^|\s)addpredict\b')
error = errors.SANDBOX_ADDPREDICT
class DeprecatedBindnowFlags(LineCheck):
"""Check for calls to the deprecated bindnow-flags function."""
repoman_check_name = 'ebuild.minorsyn'
re = re.compile(r'.*\$\(bindnow-flags\)')
error = errors.DEPRECATED_BINDNOW_FLAGS
class WantAutoDefaultValue(LineCheck):
"""Check setting WANT_AUTO* to latest (default value)."""
repoman_check_name = 'ebuild.minorsyn'
_re = re.compile(r'^WANT_AUTO(CONF|MAKE)=(\'|")?latest')
def check(self, num, line):
m = self._re.match(line)
if m is not None:
return 'WANT_AUTO' + m.group(1) + \
' redundantly set to default value "latest" on line: %d'
class SrcCompileEconf(PhaseCheck):
repoman_check_name = 'ebuild.minorsyn'
configure_re = re.compile(r'\s(econf|./configure)')
def check_eapi(self, eapi):
return eapi_has_src_prepare_and_src_configure(eapi)
def phase_check(self, num, line):
if self.in_phase == 'src_compile':
m = self.configure_re.match(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" call should be moved to src_configure from line: %d"
class SrcUnpackPatches(PhaseCheck):
repoman_check_name = 'ebuild.minorsyn'
src_prepare_tools_re = re.compile(r'\s(e?patch|sed)\s')
def check_eapi(self, eapi):
return eapi_has_src_prepare_and_src_configure(eapi)
def phase_check(self, num, line):
if self.in_phase == 'src_unpack':
m = self.src_prepare_tools_re.search(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" call should be moved to src_prepare from line: %d"
class BuiltWithUse(LineCheck):
repoman_check_name = 'ebuild.minorsyn'
re = re.compile(r'(^|.*\b)built_with_use\b')
error = errors.BUILT_WITH_USE
class DeprecatedUseq(LineCheck):
"""Checks for use of the deprecated useq function"""
repoman_check_name = 'ebuild.minorsyn'
re = re.compile(r'(^|.*\b)useq\b')
error = errors.USEQ_ERROR
class DeprecatedHasq(LineCheck):
"""Checks for use of the deprecated hasq function"""
repoman_check_name = 'ebuild.minorsyn'
re = re.compile(r'(^|.*\b)hasq\b')
error = errors.HASQ_ERROR
# EAPI <2 checks
class UndefinedSrcPrepareSrcConfigurePhases(LineCheck):
repoman_check_name = 'EAPI.incompatible'
src_configprepare_re = re.compile(r'\s*(src_configure|src_prepare)\s*\(\)')
def check_eapi(self, eapi):
return not eapi_has_src_prepare_and_src_configure(eapi)
def check(self, num, line):
m = self.src_configprepare_re.match(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" phase is not defined in EAPI < 2 on line: %d"
# EAPI-3 checks
class Eapi3DeprecatedFuncs(LineCheck):
repoman_check_name = 'EAPI.deprecated'
deprecated_commands_re = re.compile(r'^\s*(check_license)\b')
def check_eapi(self, eapi):
return eapi not in ('0', '1', '2')
def check(self, num, line):
m = self.deprecated_commands_re.match(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" has been deprecated in EAPI=3 on line: %d"
# EAPI <4 checks
class UndefinedPkgPretendPhase(LineCheck):
repoman_check_name = 'EAPI.incompatible'
pkg_pretend_re = re.compile(r'\s*(pkg_pretend)\s*\(\)')
def check_eapi(self, eapi):
return not eapi_has_pkg_pretend(eapi)
def check(self, num, line):
m = self.pkg_pretend_re.match(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" phase is not defined in EAPI < 4 on line: %d"
# EAPI-4 checks
class Eapi4IncompatibleFuncs(LineCheck):
repoman_check_name = 'EAPI.incompatible'
banned_commands_re = re.compile(r'^\s*(dosed|dohard)')
def check_eapi(self, eapi):
return not eapi_has_dosed_dohard(eapi)
def check(self, num, line):
m = self.banned_commands_re.match(line)
if m is not None:
return ("'%s'" % m.group(1)) + \
" has been banned in EAPI=4 on line: %d"
class Eapi4GoneVars(LineCheck):
repoman_check_name = 'EAPI.incompatible'
undefined_vars_re = re.compile(r'.*\$(\{(AA|KV|EMERGE_FROM)\}|(AA|KV|EMERGE_FROM))')
def check_eapi(self, eapi):
# AA, KV, and EMERGE_FROM should not be referenced in EAPI 4 or later.
return not eapi_exports_AA(eapi)
def check(self, num, line):
m = self.undefined_vars_re.match(line)
if m is not None:
return ("variable '$%s'" % m.group(1)) + \
" is gone in EAPI=4 on line: %d"
class PortageInternal(LineCheck):
repoman_check_name = 'portage.internal'
ignore_comment = True
# Match when the command is preceded only by leading whitespace or a shell
# operator such as (, {, |, ||, or &&. This prevents false positives in
# things like elog messages, as reported in bug #413285.
re = re.compile(r'^(\s*|.*[|&{(]+\s*)\b(ecompress|ecompressdir|env-update|prepall|prepalldocs|preplib)\b')
def check(self, num, line):
"""Run the check on line and return error if there is one"""
m = self.re.match(line)
if m is not None:
return ("'%s'" % m.group(2)) + " called on line: %d"
class PortageInternalVariableAssignment(LineCheck):
repoman_check_name = 'portage.internal'
internal_assignment = re.compile(r'\s*(export\s+)?(EXTRA_ECONF|EXTRA_EMAKE)\+?=')
def check(self, num, line):
match = self.internal_assignment.match(line)
e = None
if match is not None:
e = 'Assignment to variable %s' % match.group(2)
e += ' on line: %d'
return e
_base_check_classes = (InheritEclass, LineCheck, PhaseCheck)
_constant_checks = None
def _init(experimental_inherit=False):
global _constant_checks, _eclass_info
if not experimental_inherit:
# Emulate the old eprefixify.defined and inherit.autotools checks.
_eclass_info = {
'autotools': {
'funcs': (
'eaclocal', 'eautoconf', 'eautoheader',
'eautomake', 'eautoreconf', '_elibtoolize',
'eautopoint'
),
'comprehensive': True,
'ignore_missing': True,
'exempt_eclasses': ('git', 'git-2', 'subversion', 'autotools-utils')
},
'prefix': {
'funcs': (
'eprefixify',
),
'comprehensive': False
}
}
_constant_checks = tuple(chain((v() for k, v in globals().items()
if isinstance(v, type) and issubclass(v, LineCheck) and
v not in _base_check_classes),
(InheritEclass(k, **portage._native_kwargs(kwargs))
for k, kwargs in _eclass_info.items())))
_here_doc_re = re.compile(r'.*\s<<[-]?(\w+)$')
_ignore_comment_re = re.compile(r'^\s*#')
def run_checks(contents, pkg):
unicode_escape_codec = codecs.lookup('unicode_escape')
unicode_escape = lambda x: unicode_escape_codec.decode(x)[0]
if _constant_checks is None:
_init()
checks = _constant_checks
here_doc_delim = None
multiline = None
for lc in checks:
lc.new(pkg)
for num, line in enumerate(contents):
# Check if we're inside a here-document.
if here_doc_delim is not None:
if here_doc_delim.match(line):
here_doc_delim = None
if here_doc_delim is None:
here_doc = _here_doc_re.match(line)
if here_doc is not None:
here_doc_delim = re.compile(r'^\s*%s$' % here_doc.group(1))
if here_doc_delim is not None:
continue
# Unroll multiline escaped strings so that we can check things:
# inherit foo bar \
# moo \
# cow
# This will merge these lines like so:
# inherit foo bar moo cow
try:
# A normal line will end in the two bytes: <\> <\n>. So decoding
# that will result in python thinking the <\n> is being escaped
# and eat the single <\> which makes it hard for us to detect.
# Instead, strip the newline (which we know all lines have), and
# append a <0>. Then when python escapes it, if the line ended
# in a <\>, we'll end up with a <\0> marker to key off of. This
# shouldn't be a problem with any valid ebuild ...
line_escaped = unicode_escape(line.rstrip('\n') + '0')
except SystemExit:
raise
except:
# Who knows what kind of crazy crap an ebuild will have
# in it -- don't allow it to kill us.
line_escaped = line
if multiline:
# Chop off the \ and \n bytes from the previous line.
multiline = multiline[:-2] + line
if not line_escaped.endswith('\0'):
line = multiline
num = multinum
multiline = None
else:
continue
else:
if line_escaped.endswith('\0'):
multinum = num
multiline = line
continue
if not line.endswith("#nowarn\n"):
# Finally we have a full line to parse.
is_comment = _ignore_comment_re.match(line) is not None
for lc in checks:
if is_comment and lc.ignore_comment:
continue
if lc.check_eapi(pkg.eapi):
ignore = lc.ignore_line
if not ignore or not ignore.match(line):
e = lc.check(num, line)
if e:
yield lc.repoman_check_name, e % (num + 1)
for lc in checks:
i = lc.end()
if i is not None:
for e in i:
yield lc.repoman_check_name, e