Adds numpy and matplot lib (used by new_tko) to the modules built and
installed.  Refactors some common code out in the process.

Signed-off-by: Gregory Smith <[email protected]>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@2761 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/utils/build_externals.py b/utils/build_externals.py
index 7ec92fa..ce79037 100644
--- a/utils/build_externals.py
+++ b/utils/build_externals.py
@@ -47,6 +47,14 @@
     package_dir = os.path.join(top_of_tree, PACKAGE_DIR)
     install_dir = os.path.join(top_of_tree, INSTALL_DIR)
 
+    if install_dir not in sys.path:
+        # Make sure the install_dir is in our python module search path
+        # as well as the PYTHONPATH being used by all our setup.py
+        # install subprocesses.
+        sys.path.insert(0, install_dir)
+        os.environ['PYTHONPATH'] = ':'.join([
+            install_dir, os.environ.get('PYTHONPATH', '')])
+
     fetched_packages, fetch_errors = fetch_necessary_packages(package_dir)
     install_errors = build_and_install_packages(fetched_packages, install_dir)
 
@@ -55,8 +63,11 @@
     # When printing exception tracebacks, python uses that path first to look
     # for the source code before checking the directory of the .pyc file.
     # Don't leave references to our temporary build dir in the files.
-    logging.info('compiling to .pyc files in %s', install_dir)
-    compileall.compile_dir(install_dir)
+    logging.info('compiling .py files in %s to .pyc', install_dir)
+    compileall.compile_dir(install_dir, quiet=True)
+
+    # Some things install with whacky permissions, fix that.
+    system("chmod -R a+rX '%s'" % install_dir)
 
     errors = fetch_errors + install_errors
     for error_msg in errors:
@@ -76,10 +87,13 @@
                need to be installed.
              * A list of error messages for any failed fetches.
     """
+    names_to_check = sys.argv[1:]
     errors = []
     fetched_packages = []
     for package_class in ExternalPackage.subclasses:
         package = package_class()
+        if names_to_check and package.name.lower() not in names_to_check:
+            continue
         if not package.is_needed():
             logging.info('A new %s is not needed on this system.',
                          package.name)
@@ -149,6 +163,10 @@
               fetched package.
       @attribute hex_sum - The hex digest (currently SHA1) of this package
               to be used to verify its contents.
+      @attribute module_name - The installed python module name to be used for
+              for a version check.  Defaults to the lower case class name with
+              the word Package stripped off.
+      @attribyte version - The desired minimum package version.
       @attribute name - Read only, the printable name of the package.
       @attribute subclasses - This class attribute holds a list of all defined
               subclasses.  It is constructed dynamically using the metaclass.
@@ -157,22 +175,22 @@
     urls = ()
     local_filename = None
     hex_sum = None
+    module_name = None
+    version = None
 
 
     class __metaclass__(type):
         """Any time a subclass is defined, add it to our list."""
-        def __init__(cls, name, bases, dict):
+        def __init__(mcs, name, bases, dict):
             if name != 'ExternalPackage':
-                cls.subclasses.append(cls)
+                mcs.subclasses.append(mcs)
 
 
     def __init__(self):
         self.verified_package = ''
-
-
-    def is_needed(self):
-        """@returns A boolean indicating if this package is needed."""
-        return True
+        if not self.module_name:
+            self.module_name = self.name.lower()
+        self.installed_version = ''
 
 
     @property
@@ -184,6 +202,32 @@
         return class_name
 
 
+    def is_needed(self):
+        """@returns True if self.module_name needs to be built and installed."""
+        if not self.module_name or not self.version:
+            logging.warning('version and module_name required for '
+                            'is_needed() check to work.')
+            return True
+        try:
+            module = __import__(self.module_name)
+        except ImportError, e:
+            logging.info('Could not import %s.', self.module_name)
+            return True
+        self.installed_version = self._get_installed_version_from_module(module)
+        logging.info('imported %s version %s.', self.module_name,
+                     self.installed_version)
+        return self.version > self.installed_version
+
+
+    def _get_installed_version_from_module(self, module):
+        """Ask our module its version string and return it or '' if unknown."""
+        try:
+            return module.__version__
+        except AttributeError:
+            logging.error('could not get version from %s', module)
+            return ''
+
+
     def _build_and_install(self, install_dir):
         """Subclasses MUST provide their own implementation."""
         raise NotImplementedError
@@ -209,6 +253,22 @@
         return self._build_and_install(install_dir)
 
 
+    def _build_and_install_current_dir_setup_py(self, install_dir):
+        """For use as a _build_and_install_current_dir implementation."""
+        egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
+        if not egg_path:
+            return False
+        return self._install_from_egg(install_dir, egg_path)
+
+
+    def _build_and_install_current_dir_setupegg_py(self, install_dir):
+        """For use as a _build_and_install_current_dir implementation."""
+        egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
+        if not egg_path:
+            return False
+        return self._install_from_egg(install_dir, egg_path)
+
+
     def _build_and_install_from_tar_gz(self, install_dir):
         """
         This method may be used as a _build_and_install() implementation
@@ -246,35 +306,39 @@
             raise Error('tar failed with %s' % (status,))
 
 
-    def _build_using_setup_py(self):
+    def _build_using_setup_py(self, setup_py='setup.py'):
         """
         Assuming the cwd is the extracted python package, execute a simple
         python setup.py build.
 
+        @param setup_py - The name of the setup.py file to execute.
+
         @returns True on success, False otherwise.
         """
-        if not os.path.exists('setup.py'):
-            raise Error('setup.py does not exist in %s' % os.getcwd())
-        status = system("'%s' setup.py build" % (sys.executable,))
+        if not os.path.exists(setup_py):
+            raise Error('%sdoes not exist in %s' % (setup_py, os.getcwd()))
+        status = system("'%s' %s build" % (sys.executable, setup_py))
         if status:
             logging.error('%s build failed.' % self.name)
             return False
         return True
 
 
-    def _build_egg_using_setup_py(self):
+    def _build_egg_using_setup_py(self, setup_py='setup.py'):
         """
         Assuming the cwd is the extracted python package, execute a simple
         python setup.py bdist_egg.
 
+        @param setup_py - The name of the setup.py file to execute.
+
         @returns The relative path to the resulting egg file or '' on failure.
         """
-        if not os.path.exists('setup.py'):
-            raise Error('setup.py does not exist in %s' % os.getcwd())
+        if not os.path.exists(setup_py):
+            raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
         egg_subdir = 'dist'
         if os.path.isdir(egg_subdir):
             shutil.rmtree(egg_subdir)
-        status = system("'%s' setup.py bdist_egg" % (sys.executable,))
+        status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
         if status:
             logging.error('bdist_egg of setuptools failed.')
             return ''
@@ -302,7 +366,8 @@
         return True
 
 
-    def _install_using_setup_py_and_rsync(self, install_dir):
+    def _install_using_setup_py_and_rsync(self, install_dir,
+                                          setup_py='setup.py'):
         """
         Assuming the cwd is the extracted python package, execute a simple:
 
@@ -317,23 +382,21 @@
         site-packages directly up into install_dir itself.
 
         @param install_dir the directory for the install to happen under.
+        @param setup_py - The name of the setup.py file to execute.
 
         @returns True on success, False otherwise.
         """
-        if not os.path.exists('setup.py'):
-            raise Error('No setup.py exists in %s' % os.getcwd())
+        if not os.path.exists(setup_py):
+            raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
 
         temp_dir = tempfile.mkdtemp(dir='/var/tmp')
         try:
-            status = system("'%s' setup.py install --no-compile --prefix='%s'"
-                            % (sys.executable, temp_dir))
+            status = system("'%s' %s install --no-compile --prefix='%s'"
+                            % (sys.executable, setup_py, temp_dir))
             if status:
-                logging.error('%s setup.py install failed.' % self.name)
+                logging.error('%s install failed.' % self.name)
                 return False
 
-            # Some things installs itself with whacky permissions, fix that.
-            system("chmod -R a+rX '%s'" % temp_dir)
-
             # This makes assumptions about what python setup.py install
             # does when given a prefix.  Is this always correct?
             python_xy = 'python%s' % sys.version[:3]
@@ -428,10 +491,13 @@
             return False
 
 
-# NOTE: this class definition must come -before- all other ExternalPackage
+# NOTE: This class definition must come -before- all other ExternalPackage
 # classes that need to use this version of setuptools so that is is inserted
 # into the ExternalPackage.subclasses list before them.
 class SetuptoolsPackage(ExternalPackage):
+    # For all known setuptools releases a string compare works for the
+    # version string.  Hopefully they never release a 0.10.  (Their own
+    # version comparison code would break if they did.)
     version = '0.6c9'
     urls = ('http://pypi.python.org/packages/source/s/setuptools/'
             'setuptools-%s.tar.gz' % (version,),)
@@ -441,18 +507,6 @@
     SUDO_SLEEP_DELAY = 15
 
 
-    def is_needed(self):
-        """@returns True if setuptools needs to be built and installed."""
-        try:
-            import setuptools
-        except ImportError:
-            return True
-        # For all known setuptools releases a string compare works for the
-        # version string.  Hopefully they never release a 0.10.  (Their own
-        # version comparison code would break if they did.)
-        return self.version > setuptools.__version__
-
-
     def _build_and_install(self, install_dir):
         """Install setuptools on the system."""
         logging.info('NOTE: setuptools install does not use install_dir.')
@@ -488,6 +542,7 @@
 
 
 class MySQLdbPackage(ExternalPackage):
+    module_name = 'MySQLdb'
     version = '1.2.2'
     urls = ('http://dl.sourceforge.net/sourceforge/mysql-python/'
             'MySQL-python-%s.tar.gz' % (version,),)
@@ -495,41 +550,56 @@
     hex_sum = '945a04773f30091ad81743f9eb0329a3ee3de383'
 
     _build_and_install = ExternalPackage._build_and_install_from_tar_gz
-
-
-    def is_needed(self):
-        """@returns True if MySQLdb needs to be built and installed."""
-        try:
-            import MySQLdb
-        except ImportError:
-            return True
-        # Demand the exact version.
-        # I don't trust different MySQLdb versions to not break the APIs as
-        # they have in the past within 1.2.1 versions and going to 1.2.2.
-        return self.version != MySQLdb.__version__
-
-
-    def _build_and_install_current_dir(self, install_dir):
-        egg_path = self._build_egg_using_setup_py()
-        if not egg_path:
-            return False
-        return self._install_from_egg(install_dir, egg_path)
+    _build_and_install_current_dir = (
+            ExternalPackage._build_and_install_current_dir_setup_py)
 
 
 class DjangoPackage(ExternalPackage):
-    urls = ('http://media.djangoproject.com/releases/'
-            '1.0.2/Django-1.0.2-final.tar.gz',)
-    local_filename = 'Django-1.0.2-final.tar.gz'
+    version = '1.0.2'
+    local_filename = 'Django-%s-final.tar.gz' % version
+    urls = ('http://media.djangoproject.com/releases/%s/%s'
+            % (version, local_filename),)
     hex_sum = 'f2d9088f17aff47ea17e5767740cab67b2a73b6b'
 
     _build_and_install = ExternalPackage._build_and_install_from_tar_gz
 
 
+    def _get_installed_version_from_module(self, module):
+        return module.get_version().split()[0]
+
+
     def _build_and_install_current_dir(self, install_dir):
         if not self._build_using_setup_py():
             return False
+        # unlike the others, django doesn't use an Egg.
         return self._install_using_setup_py_and_rsync(install_dir)
 
 
+class NumpyPackage(ExternalPackage):
+    version = '1.2.1'
+    local_filename = 'numpy-%s.tar.gz' % version
+    urls = ('http://dl.sourceforge.net/sourceforge/numpy/' +
+            local_filename,)
+    hex_sum = '1aa706e733aea18eaffa70d93c0105718acb66c5'
+
+    _build_and_install = ExternalPackage._build_and_install_from_tar_gz
+    _build_and_install_current_dir = (
+            ExternalPackage._build_and_install_current_dir_setupegg_py)
+
+
+# This requires numpy so it must be declared after numpy to guarantee that it
+# is already installed.
+class MatplotlibPackage(ExternalPackage):
+    version = '0.98.5.2'
+    urls = ('http://dl.sourceforge.net/sourceforge/matplotlib/'
+            'matplotlib-%s.tar.gz' % (version,),)
+    local_filename = 'matplotlib-%s.tar.gz' % version
+    hex_sum = 'fbce043555de4f5a34e2a47e200527720a90b370'
+
+    _build_and_install = ExternalPackage._build_and_install_from_tar_gz
+    _build_and_install_current_dir = (
+            ExternalPackage._build_and_install_current_dir_setupegg_py)
+
+
 if __name__ == '__main__':
     sys.exit(main())