| # 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' |
| cvs_header = re.compile(r'^# \$Header: .*\$$') |
| 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: |
| if not self.cvs_header.match(line): |
| return errors.CVS_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 = [] |
| self._indirect_deprecated = set(eclass for eclass in \ |
| self.deprecated_classes if eclass in pkg.inherited) |
| |
| 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._indirect_deprecated.discard(eclass) |
| self._errors.append("please migrate from " + \ |
| "'%s' (no replacement) on line: %d" % (eclass, num + 1)) |
| else: |
| self._indirect_deprecated.discard(eclass) |
| 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 |
| |
| for eclass in self._indirect_deprecated: |
| replacement = self.deprecated_classes[eclass] |
| if replacement is False: |
| yield "please migrate from indirect " + \ |
| "inherit of '%s' (no replacement)" % (eclass,) |
| else: |
| yield "please migrate from indirect " + \ |
| "inherit of '%s' to '%s'" % \ |
| (eclass, replacement) |
| del self._indirect_deprecated |
| |
| 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 |