>From ee9b26df68fe60770df5199bf211fa5c50002e46 Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <praiskup@redhat.com>
Date: Thu, 14 Mar 2013 09:04:34 +0100
Subject: [PATCH] autotools: initial autotools plugin commit

Add plugin for autotool-related mistake checkers.  For now, only checker for
obsoleted macros is added.

* plugins/autotools.py: New file.
---
 plugins/autotools.py | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 305 insertions(+)
 create mode 100644 plugins/autotools.py

diff --git a/plugins/autotools.py b/plugins/autotools.py
new file mode 100644
index 0000000..4ff819f
--- /dev/null
+++ b/plugins/autotools.py
@@ -0,0 +1,305 @@
+#-*- coding: utf-8 -*-
+
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# (C) 2013 - Pavel Raiskup <praiskup@redhat.com>
+
+''' Autotools SHOULD checks, default Generic group. '''
+
+import re
+import textwrap
+from subprocess import Popen, PIPE
+
+from FedoraReview import CheckBase
+from FedoraReview import RegistryBase
+
+
+###############################################
+## DATABASE OF COMMONLY USED OBSOLETE MACROS ##
+##                                           ##
+## Any suggestion for newer check here would ##
+## be appreciated.                           ##
+###############################################
+
+DEF_OBS_MESSAGE = "Suggested substitution is safe for all currently " \
+                + "supported Fedora/RHEL versions."
+
+OBS_M4S_AUTOMAKE = {
+    'AM_CONFIG_HEADER': {
+        'suggest': 'AC_CONFIG_HEADERS',
+        'comment':
+            """
+            This macro was moved to autoconf package because there is more
+            appropriate place for it (for that reason the AM ~> AC prefix
+            change). Also, in newer macro is possible to pass multiple
+            directories.
+            """ + DEF_OBS_MESSAGE,
+    },
+    'AM_PROG_CC_STDC': {
+        'suggest': 'AC_PROG_CC',
+        'comment':
+            """
+            This functionality was moved to more suitable place ~> to autoconf.
+            """ + DEF_OBS_MESSAGE
+    }
+}
+
+OBS_M4_LIBTOOL_LT_INIT = {
+    'suggest': 'LT_INIT',
+    'comment':
+        """
+        This macro substitution is safe when you are packaging this for
+        EPEL >= EPEL6.
+        """
+}
+
+OBS_M4S_LIBTOOL = {
+    'AC_PROG_LIBTOOL': OBS_M4_LIBTOOL_LT_INIT,
+    'AM_PROG_LIBTOOL': OBS_M4_LIBTOOL_LT_INIT,
+}
+
+OBSOLETE_CHECKS = {
+    'automake': OBS_M4S_AUTOMAKE,
+    'libtool': OBS_M4S_LIBTOOL,
+}
+
+## DATABASE OF COMMONLY USED OBSOLETE MACROS ##
+###############################################
+
+
+class Registry(RegistryBase):
+    ''' Module registration, register all checks. '''
+    group = 'Generic'
+
+    def is_applicable(self):
+        return True
+
+
+class AutotoolsCheckBase(CheckBase):
+    ''' Base class for all Autotool-related tests. '''
+
+    used_tools = None
+
+    def __init__(self, checks):
+        CheckBase.__init__(self, checks, __file__)
+
+        # construct text wrapper
+        self.wrapper = textwrap.TextWrapper(break_long_words=False,
+                drop_whitespace=True, replace_whitespace=True,
+                fix_sentence_endings=True)
+
+    def text_wrap(self, text, width=76):
+        ''' wrap the text on the 76th character '''
+        self.wrapper.width = width
+        return self.wrapper.fill(text)
+
+    def find_used_tools(self):
+        ''' get the list of autotools relevant for this package '''
+        brequires = self.spec.build_requires
+
+        if self.used_tools is not None:
+            return
+
+        self.used_tools = []
+
+        def check_for(tool, packages):
+            ''' helper - try all known package names for the tool '''
+            for name in packages:
+                if name in brequires:
+                    self.used_tools.append(tool)
+                    return
+
+        am_pkgs = ['automake', 'automake14', 'automake15', 'automake16',
+                   'automake17']
+        check_for('automake', am_pkgs)
+        check_for('autoconf', ['autoconf', 'autoconf213'])
+        check_for('libtool', ['libtool'])
+
+        self.log.debug("autotools used: " + ' '.join(self.used_tools))
+
+
+## TEXT HELPERS ##
+
+def prepend_indent(text):
+    ''' add the paragraph indentation '''
+    lines = text.splitlines()
+    return '\n'.join(map(lambda x: " | " + x if x != "" else " |" + x, lines))
+
+
+def clean_whitespace(text):
+    ''' shrink white-space areas and remove initial whitespace '''
+    text = re.sub(r"\s+", " ", text)
+    return re.sub(r"^\s", "", text)
+
+
+## CHECKERS ##
+
+class CheckAutotoolsObsoleteMarcos(AutotoolsCheckBase):
+    ''' obsolete macros (shorthly m4s) checker '''
+
+    warn_items = {}
+
+    def __init__(self, base):
+        AutotoolsCheckBase.__init__(self, base)
+
+        # basic settings
+        self.text = 'Checking for obsolete m4s in GNU build system.'
+        self.automatic = True
+        self.type = 'SHOULD'
+        self.err_db = {}
+
+    def construct_err_db(self):
+        ''' construct database of possible errors for particular package '''
+        err_db = {}
+        for tool in self.used_tools:
+            if not tool in OBSOLETE_CHECKS:
+                continue
+            checks = OBSOLETE_CHECKS[tool]
+            for obs_m4 in checks.keys():
+                err_db[obs_m4] = checks[obs_m4]
+                err_db[obs_m4]['tool'] = tool
+
+        return err_db
+
+    def get_trace_command(self):
+        ''' construct the basic grep command '''
+        trace_cmd = ["grep", "-E", "-n", "-o"]
+
+        for tool in self.used_tools:
+            if not tool in OBSOLETE_CHECKS:
+                # shouldn't be neccessary
+                continue
+
+            checks = OBSOLETE_CHECKS[tool]
+            for obs_m4 in checks.keys():
+                trace_cmd.append("-e")
+                trace_cmd.append(obs_m4 + "[[:space:]]*$")
+                trace_cmd.append("-e")
+                trace_cmd.append(obs_m4 + r"\(")
+
+        return trace_cmd
+
+    def trace(self):
+        ''' trace for obsoleted macros '''
+
+        # find traced files
+        src = self.checks.buildsrc
+        trace_files = src.find_all('*configure.ac') \
+                    + src.find_all('*configure.in')
+
+        # construct error database
+        self.err_db = self.construct_err_db()
+
+        # get the base tracing command (grep)
+        trace_cmd = self.get_trace_command()
+
+        def shorter_configure(configure, prefix):
+            ''' remove the workdir prefix from configure file '''
+            simple = configure
+            if configure.startswith(prefix):
+                simple = configure[len(prefix) + 1:]
+            return simple
+
+        # ---------------------------
+        # search for obsoleted macros
+        # ---------------------------
+        for configure_ac in trace_files:
+            cmd = trace_cmd + [configure_ac]
+
+            try:
+                self.log.debug("running: " + ' '.join(cmd))
+                p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+                stdout, stderr = p.communicate()
+            except IOError:
+                self.set_passed(self.PENDING,
+                        "error while tracing autoconf.ac")
+                return
+
+            if not p.returncode in [0, 1]:
+                msg = "grep returns bad exit value %d: " % p.returncode \
+                    + stderr
+                self.set_passed(self.PENDING, msg)
+                return
+
+            m4_lines = stdout.splitlines(False)
+            m4_lines = map(lambda x: x.strip('('), m4_lines)
+            for m4_line in m4_lines:
+                line, m4 = m4_line.split(':')
+
+                if not m4 in self.warn_items:
+                    self.warn_items[m4] = []
+
+                self.warn_items[m4].append({
+                    'file': shorter_configure(configure_ac, src.containers[0]),
+                    'line': int(line),
+                })
+
+    def generate_pretty_output(self):
+        '''
+        take the results from self.warn_items and generate at pretty looking
+        messages (the maximum width is 79 bytes).
+        '''
+        output = "Autotools check:\n================\n\n"
+        desc = """ There is suggested to use -Wobsolete option while running
+        the autoconf, automake, autoreconf, ... if you want to see relevant
+        warnings. Consider running autopoint but be careful when you are trying
+        to prepare package for older distributions with not so up2date
+        tools."""
+        output += self.text_wrap(clean_whitespace(desc), 79) + '\n\n'
+
+        for item in self.warn_items.keys():
+            desc = self.err_db[item]
+            positions = self.warn_items[item]
+            output = output + "[" + item + "] found, substitute it by [" \
+                   + desc['suggest'] + "] (tool: " + desc['tool'] + ")\n\n"
+
+            hit = "Found in:\n" \
+                + "---------\n"
+
+            for pos in positions:
+                hit = hit + pos['file'] + ':' + str(pos['line']) + '\n'
+
+            if "comment" in desc:
+                hit = hit + "\n" \
+                    + "Additional comment\n" \
+                    + "------------------\n" \
+                    + self.text_wrap(clean_whitespace(desc['comment']))
+
+            hit = prepend_indent(hit)
+            output = output + hit + '\n\n'
+
+        return output
+
+    def run(self):
+        ''' standard entry point for each check '''
+        self.set_passed(self.NA)
+
+        self.find_used_tools()
+        if not self.used_tools:
+            return
+
+        # trace for warnings
+        self.trace()
+
+        if not len(self.warn_items):
+            self.set_passed(self.PASS)
+            return
+
+        msg = "Some obsoleted macros found, see the attachment."
+        output = self.generate_pretty_output()
+        self.set_passed(self.FAIL, msg, [output])
+        return
+
+# vim: set expandtab ts=4 sw=4 tw=79:
-- 
1.8.1.4

