Home | History | Annotate | Download | only in utils
      1 # Please keep this code python 2.4 compatible and stand alone.
      2 
      3 import logging, os, shutil, sys, tempfile, time, urllib2
      4 import subprocess, re
      5 from autotest_lib.client.common_lib import autotemp, revision_control, utils
      6 
      7 _READ_SIZE = 64*1024
      8 _MAX_PACKAGE_SIZE = 100*1024*1024
      9 
     10 
     11 class Error(Exception):
     12     """Local exception to be raised by code in this file."""
     13 
     14 class FetchError(Error):
     15     """Failed to fetch a package from any of its listed URLs."""
     16 
     17 
     18 def _checksum_file(full_path):
     19     """@returns The hex checksum of a file given its pathname."""
     20     inputfile = open(full_path, 'rb')
     21     try:
     22         hex_sum = utils.hash('sha1', inputfile.read()).hexdigest()
     23     finally:
     24         inputfile.close()
     25     return hex_sum
     26 
     27 
     28 def system(commandline):
     29     """Same as os.system(commandline) but logs the command first.
     30 
     31     @param commandline: commandline to be called.
     32     """
     33     logging.info(commandline)
     34     return os.system(commandline)
     35 
     36 
     37 def find_top_of_autotest_tree():
     38     """@returns The full path to the top of the autotest directory tree."""
     39     dirname = os.path.dirname(__file__)
     40     autotest_dir = os.path.abspath(os.path.join(dirname, '..'))
     41     return autotest_dir
     42 
     43 
     44 class ExternalPackage(object):
     45     """
     46     Defines an external package with URLs to fetch its sources from and
     47     a build_and_install() method to unpack it, build it and install it
     48     beneath our own autotest/site-packages directory.
     49 
     50     Base Class.  Subclass this to define packages.
     51     Note: Unless your subclass has a specific reason to, it should not
     52     re-install the package every time build_externals is invoked, as this
     53     happens periodically through the scheduler. To avoid doing so the is_needed
     54     method needs to return an appropriate value.
     55 
     56     Attributes:
     57       @attribute urls - A tuple of URLs to try fetching the package from.
     58       @attribute local_filename - A local filename to use when saving the
     59               fetched package.
     60       @attribute hex_sum - The hex digest (currently SHA1) of this package
     61               to be used to verify its contents.
     62       @attribute module_name - The installed python module name to be used for
     63               for a version check.  Defaults to the lower case class name with
     64               the word Package stripped off.
     65       @attribute version - The desired minimum package version.
     66       @attribute os_requirements - A dictionary mapping pathname tuples on the
     67               the OS distribution to a likely name of a package the user
     68               needs to install on their system in order to get this file.
     69               One of the files in the tuple must exist.
     70       @attribute name - Read only, the printable name of the package.
     71       @attribute subclasses - This class attribute holds a list of all defined
     72               subclasses.  It is constructed dynamically using the metaclass.
     73     """
     74     # Modules that are meant to be installed in system directory, rather than
     75     # autotest/site-packages. These modules should be skipped if the module
     76     # is already installed in system directory. This prevents an older version
     77     # of the module from being installed in system directory.
     78     SYSTEM_MODULES = ['setuptools']
     79 
     80     subclasses = []
     81     urls = ()
     82     local_filename = None
     83     hex_sum = None
     84     module_name = None
     85     version = None
     86     os_requirements = None
     87 
     88 
     89     class __metaclass__(type):
     90         """Any time a subclass is defined, add it to our list."""
     91         def __init__(mcs, name, bases, dict):
     92             if name != 'ExternalPackage' and not name.startswith('_'):
     93                 mcs.subclasses.append(mcs)
     94 
     95 
     96     def __init__(self):
     97         self.verified_package = ''
     98         if not self.module_name:
     99             self.module_name = self.name.lower()
    100         self.installed_version = ''
    101 
    102 
    103     @property
    104     def name(self):
    105         """Return the class name with any trailing 'Package' stripped off."""
    106         class_name = self.__class__.__name__
    107         if class_name.endswith('Package'):
    108             return class_name[:-len('Package')]
    109         return class_name
    110 
    111 
    112     def is_needed(self, install_dir):
    113         """
    114         Check to see if we need to reinstall a package. This is contingent on:
    115         1. Module name: If the name of the module is different from the package,
    116             the class that installs it needs to specify a module_name string,
    117             so we can try importing the module.
    118 
    119         2. Installed version: If the module doesn't contain a __version__ the
    120             class that installs it needs to override the
    121             _get_installed_version_from_module method to return an appropriate
    122             version string.
    123 
    124         3. Version/Minimum version: The class that installs the package should
    125             contain a version string, and an optional minimum version string.
    126 
    127         4. install_dir: If the module exists in a different directory, e.g.,
    128             /usr/lib/python2.7/dist-packages/, the module will be forced to be
    129             installed in install_dir.
    130 
    131         @param install_dir: install directory.
    132         @returns True if self.module_name needs to be built and installed.
    133         """
    134         if not self.module_name or not self.version:
    135             logging.warning('version and module_name required for '
    136                             'is_needed() check to work.')
    137             return True
    138         try:
    139             module = __import__(self.module_name)
    140         except ImportError, e:
    141             logging.info("%s isn't present. Will install.", self.module_name)
    142             return True
    143         if (not module.__file__.startswith(install_dir) and
    144             not self.module_name in self.SYSTEM_MODULES):
    145             logging.info('Module %s is installed in %s, rather than %s. The '
    146                          'module will be forced to be installed in %s.',
    147                          self.module_name, module.__file__, install_dir,
    148                          install_dir)
    149             return True
    150         self.installed_version = self._get_installed_version_from_module(module)
    151         logging.info('imported %s version %s.', self.module_name,
    152                      self.installed_version)
    153         if hasattr(self, 'minimum_version'):
    154             return self.minimum_version > self.installed_version
    155         else:
    156             return self.version > self.installed_version
    157 
    158 
    159     def _get_installed_version_from_module(self, module):
    160         """Ask our module its version string and return it or '' if unknown."""
    161         try:
    162             return module.__version__
    163         except AttributeError:
    164             logging.error('could not get version from %s', module)
    165             return ''
    166 
    167 
    168     def _build_and_install(self, install_dir):
    169         """Subclasses MUST provide their own implementation."""
    170         raise NotImplementedError
    171 
    172 
    173     def _build_and_install_current_dir(self, install_dir):
    174         """
    175         Subclasses that use _build_and_install_from_package() MUST provide
    176         their own implementation of this method.
    177         """
    178         raise NotImplementedError
    179 
    180 
    181     def build_and_install(self, install_dir):
    182         """
    183         Builds and installs the package.  It must have been fetched already.
    184 
    185         @param install_dir - The package installation directory.  If it does
    186             not exist it will be created.
    187         """
    188         if not self.verified_package:
    189             raise Error('Must call fetch() first.  - %s' % self.name)
    190         self._check_os_requirements()
    191         return self._build_and_install(install_dir)
    192 
    193 
    194     def _check_os_requirements(self):
    195         if not self.os_requirements:
    196             return
    197         failed = False
    198         for file_names, package_name in self.os_requirements.iteritems():
    199             if not any(os.path.exists(file_name) for file_name in file_names):
    200                 failed = True
    201                 logging.error('Can\'t find %s, %s probably needs it.',
    202                               ' or '.join(file_names), self.name)
    203                 logging.error('Perhaps you need to install something similar '
    204                               'to the %s package for OS first.', package_name)
    205         if failed:
    206             raise Error('Missing OS requirements for %s.  (see above)' %
    207                         self.name)
    208 
    209 
    210     def _build_and_install_current_dir_setup_py(self, install_dir):
    211         """For use as a _build_and_install_current_dir implementation."""
    212         egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
    213         if not egg_path:
    214             return False
    215         return self._install_from_egg(install_dir, egg_path)
    216 
    217 
    218     def _build_and_install_current_dir_setupegg_py(self, install_dir):
    219         """For use as a _build_and_install_current_dir implementation."""
    220         egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
    221         if not egg_path:
    222             return False
    223         return self._install_from_egg(install_dir, egg_path)
    224 
    225 
    226     def _build_and_install_current_dir_noegg(self, install_dir):
    227         if not self._build_using_setup_py():
    228             return False
    229         return self._install_using_setup_py_and_rsync(install_dir)
    230 
    231 
    232     def _build_and_install_from_package(self, install_dir):
    233         """
    234         This method may be used as a _build_and_install() implementation
    235         for subclasses if they implement _build_and_install_current_dir().
    236 
    237         Extracts the .tar.gz file, chdirs into the extracted directory
    238         (which is assumed to match the tar filename) and calls
    239         _build_and_isntall_current_dir from there.
    240 
    241         Afterwards the build (regardless of failure) extracted .tar.gz
    242         directory is cleaned up.
    243 
    244         @returns True on success, False otherwise.
    245 
    246         @raises OSError If the expected extraction directory does not exist.
    247         """
    248         self._extract_compressed_package()
    249         if self.verified_package.endswith('.tar.gz'):
    250             extension = '.tar.gz'
    251         elif self.verified_package.endswith('.tar.bz2'):
    252             extension = '.tar.bz2'
    253         elif self.verified_package.endswith('.zip'):
    254             extension = '.zip'
    255         else:
    256             raise Error('Unexpected package file extension on %s' %
    257                         self.verified_package)
    258         os.chdir(os.path.dirname(self.verified_package))
    259         os.chdir(self.local_filename[:-len(extension)])
    260         extracted_dir = os.getcwd()
    261         try:
    262             return self._build_and_install_current_dir(install_dir)
    263         finally:
    264             os.chdir(os.path.join(extracted_dir, '..'))
    265             shutil.rmtree(extracted_dir)
    266 
    267 
    268     def _extract_compressed_package(self):
    269         """Extract the fetched compressed .tar or .zip within its directory."""
    270         if not self.verified_package:
    271             raise Error('Package must have been fetched first.')
    272         os.chdir(os.path.dirname(self.verified_package))
    273         if self.verified_package.endswith('gz'):
    274             status = system("tar -xzf '%s'" % self.verified_package)
    275         elif self.verified_package.endswith('bz2'):
    276             status = system("tar -xjf '%s'" % self.verified_package)
    277         elif self.verified_package.endswith('zip'):
    278             status = system("unzip '%s'" % self.verified_package)
    279         else:
    280             raise Error('Unknown compression suffix on %s.' %
    281                         self.verified_package)
    282         if status:
    283             raise Error('tar failed with %s' % (status,))
    284 
    285 
    286     def _build_using_setup_py(self, setup_py='setup.py'):
    287         """
    288         Assuming the cwd is the extracted python package, execute a simple
    289         python setup.py build.
    290 
    291         @param setup_py - The name of the setup.py file to execute.
    292 
    293         @returns True on success, False otherwise.
    294         """
    295         if not os.path.exists(setup_py):
    296             raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
    297         status = system("'%s' %s build" % (sys.executable, setup_py))
    298         if status:
    299             logging.error('%s build failed.', self.name)
    300             return False
    301         return True
    302 
    303 
    304     def _build_egg_using_setup_py(self, setup_py='setup.py'):
    305         """
    306         Assuming the cwd is the extracted python package, execute a simple
    307         python setup.py bdist_egg.
    308 
    309         @param setup_py - The name of the setup.py file to execute.
    310 
    311         @returns The relative path to the resulting egg file or '' on failure.
    312         """
    313         if not os.path.exists(setup_py):
    314             raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
    315         egg_subdir = 'dist'
    316         if os.path.isdir(egg_subdir):
    317             shutil.rmtree(egg_subdir)
    318         status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
    319         if status:
    320             logging.error('bdist_egg of setuptools failed.')
    321             return ''
    322         # I've never seen a bdist_egg lay multiple .egg files.
    323         for filename in os.listdir(egg_subdir):
    324             if filename.endswith('.egg'):
    325                 return os.path.join(egg_subdir, filename)
    326 
    327 
    328     def _install_from_egg(self, install_dir, egg_path):
    329         """
    330         Install a module from an egg file by unzipping the necessary parts
    331         into install_dir.
    332 
    333         @param install_dir - The installation directory.
    334         @param egg_path - The pathname of the egg file.
    335         """
    336         status = system("unzip -q -o -d '%s' '%s'" % (install_dir, egg_path))
    337         if status:
    338             logging.error('unzip of %s failed', egg_path)
    339             return False
    340         egg_info = os.path.join(install_dir, 'EGG-INFO')
    341         if os.path.isdir(egg_info):
    342             shutil.rmtree(egg_info)
    343         return True
    344 
    345 
    346     def _get_temp_dir(self):
    347         return tempfile.mkdtemp(dir='/var/tmp')
    348 
    349 
    350     def _site_packages_path(self, temp_dir):
    351         # This makes assumptions about what python setup.py install
    352         # does when given a prefix.  Is this always correct?
    353         python_xy = 'python%s' % sys.version[:3]
    354         return os.path.join(temp_dir, 'lib', python_xy, 'site-packages')
    355 
    356 
    357     def _rsync (self, temp_site_dir, install_dir):
    358         """Rsync contents. """
    359         status = system("rsync -r '%s/' '%s/'" %
    360                         (os.path.normpath(temp_site_dir),
    361                          os.path.normpath(install_dir)))
    362         if status:
    363             logging.error('%s rsync to install_dir failed.', self.name)
    364             return False
    365         return True
    366 
    367 
    368     def _install_using_setup_py_and_rsync(self, install_dir,
    369                                           setup_py='setup.py',
    370                                           temp_dir=None):
    371         """
    372         Assuming the cwd is the extracted python package, execute a simple:
    373 
    374           python setup.py install --prefix=BLA
    375 
    376         BLA will be a temporary directory that everything installed will
    377         be picked out of and rsynced to the appropriate place under
    378         install_dir afterwards.
    379 
    380         Afterwards, it deconstructs the extra lib/pythonX.Y/site-packages/
    381         directory tree that setuptools created and moves all installed
    382         site-packages directly up into install_dir itself.
    383 
    384         @param install_dir the directory for the install to happen under.
    385         @param setup_py - The name of the setup.py file to execute.
    386 
    387         @returns True on success, False otherwise.
    388         """
    389         if not os.path.exists(setup_py):
    390             raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
    391 
    392         if temp_dir is None:
    393             temp_dir = self._get_temp_dir()
    394 
    395         try:
    396             status = system("'%s' %s install --no-compile --prefix='%s'"
    397                             % (sys.executable, setup_py, temp_dir))
    398             if status:
    399                 logging.error('%s install failed.', self.name)
    400                 return False
    401 
    402             if os.path.isdir(os.path.join(temp_dir, 'lib')):
    403                 # NOTE: This ignores anything outside of the lib/ dir that
    404                 # was installed.
    405                 temp_site_dir = self._site_packages_path(temp_dir)
    406             else:
    407                 temp_site_dir = temp_dir
    408 
    409             return self._rsync(temp_site_dir, install_dir)
    410         finally:
    411             shutil.rmtree(temp_dir)
    412 
    413 
    414 
    415     def _build_using_make(self, install_dir):
    416         """Build the current package using configure/make.
    417 
    418         @returns True on success, False otherwise.
    419         """
    420         install_prefix = os.path.join(install_dir, 'usr', 'local')
    421         status = system('./configure --prefix=%s' % install_prefix)
    422         if status:
    423             logging.error('./configure failed for %s', self.name)
    424             return False
    425         status = system('make')
    426         if status:
    427             logging.error('make failed for %s', self.name)
    428             return False
    429         status = system('make check')
    430         if status:
    431             logging.error('make check failed for %s', self.name)
    432             return False
    433         return True
    434 
    435 
    436     def _install_using_make(self):
    437         """Install the current package using make install.
    438 
    439         Assumes the install path was set up while running ./configure (in
    440         _build_using_make()).
    441 
    442         @returns True on success, False otherwise.
    443         """
    444         status = system('make install')
    445         return status == 0
    446 
    447 
    448     def fetch(self, dest_dir):
    449         """
    450         Fetch the package from one its URLs and save it in dest_dir.
    451 
    452         If the the package already exists in dest_dir and the checksum
    453         matches this code will not fetch it again.
    454 
    455         Sets the 'verified_package' attribute with the destination pathname.
    456 
    457         @param dest_dir - The destination directory to save the local file.
    458             If it does not exist it will be created.
    459 
    460         @returns A boolean indicating if we the package is now in dest_dir.
    461         @raises FetchError - When something unexpected happens.
    462         """
    463         if not os.path.exists(dest_dir):
    464             os.makedirs(dest_dir)
    465         local_path = os.path.join(dest_dir, self.local_filename)
    466 
    467         # If the package exists, verify its checksum and be happy if it is good.
    468         if os.path.exists(local_path):
    469             actual_hex_sum = _checksum_file(local_path)
    470             if self.hex_sum == actual_hex_sum:
    471                 logging.info('Good checksum for existing %s package.',
    472                              self.name)
    473                 self.verified_package = local_path
    474                 return True
    475             logging.warning('Bad checksum for existing %s package.  '
    476                             'Re-downloading', self.name)
    477             os.rename(local_path, local_path + '.wrong-checksum')
    478 
    479         # Download the package from one of its urls, rejecting any if the
    480         # checksum does not match.
    481         for url in self.urls:
    482             logging.info('Fetching %s', url)
    483             try:
    484                 url_file = urllib2.urlopen(url)
    485             except (urllib2.URLError, EnvironmentError):
    486                 logging.warning('Could not fetch %s package from %s.',
    487                                 self.name, url)
    488                 continue
    489 
    490             data_length = int(url_file.info().get('Content-Length',
    491                                                   _MAX_PACKAGE_SIZE))
    492             if data_length <= 0 or data_length > _MAX_PACKAGE_SIZE:
    493                 raise FetchError('%s from %s fails Content-Length %d '
    494                                  'sanity check.' % (self.name, url,
    495                                                     data_length))
    496             checksum = utils.hash('sha1')
    497             total_read = 0
    498             output = open(local_path, 'wb')
    499             try:
    500                 while total_read < data_length:
    501                     data = url_file.read(_READ_SIZE)
    502                     if not data:
    503                         break
    504                     output.write(data)
    505                     checksum.update(data)
    506                     total_read += len(data)
    507             finally:
    508                 output.close()
    509             if self.hex_sum != checksum.hexdigest():
    510                 logging.warning('Bad checksum for %s fetched from %s.',
    511                                 self.name, url)
    512                 logging.warning('Got %s', checksum.hexdigest())
    513                 logging.warning('Expected %s', self.hex_sum)
    514                 os.unlink(local_path)
    515                 continue
    516             logging.info('Good checksum.')
    517             self.verified_package = local_path
    518             return True
    519         else:
    520             return False
    521 
    522 
    523 # NOTE: This class definition must come -before- all other ExternalPackage
    524 # classes that need to use this version of setuptools so that is is inserted
    525 # into the ExternalPackage.subclasses list before them.
    526 class SetuptoolsPackage(ExternalPackage):
    527     """setuptools package"""
    528     # For all known setuptools releases a string compare works for the
    529     # version string.  Hopefully they never release a 0.10.  (Their own
    530     # version comparison code would break if they did.)
    531     # Any system with setuptools > 18.0.1 is fine. If none installed, then
    532     # try to install the latest found on the upstream.
    533     minimum_version = '18.0.1'
    534     version = '18.0.1'
    535     urls = ('http://pypi.python.org/packages/source/s/setuptools/'
    536             'setuptools-%s.tar.gz' % (version,),)
    537     local_filename = 'setuptools-%s.tar.gz' % version
    538     hex_sum = 'ebc4fe81b7f6d61d923d9519f589903824044f52'
    539 
    540     SUDO_SLEEP_DELAY = 15
    541 
    542 
    543     def _build_and_install(self, install_dir):
    544         """Install setuptools on the system."""
    545         logging.info('NOTE: setuptools install does not use install_dir.')
    546         return self._build_and_install_from_package(install_dir)
    547 
    548 
    549     def _build_and_install_current_dir(self, install_dir):
    550         egg_path = self._build_egg_using_setup_py()
    551         if not egg_path:
    552             return False
    553 
    554         print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
    555         print 'About to run sudo to install setuptools', self.version
    556         print 'on your system for use by', sys.executable, '\n'
    557         print '!! ^C within', self.SUDO_SLEEP_DELAY, 'seconds to abort.\n'
    558         time.sleep(self.SUDO_SLEEP_DELAY)
    559 
    560         # Copy the egg to the local filesystem /var/tmp so that root can
    561         # access it properly (avoid NFS squashroot issues).
    562         temp_dir = self._get_temp_dir()
    563         try:
    564             shutil.copy(egg_path, temp_dir)
    565             egg_name = os.path.split(egg_path)[1]
    566             temp_egg = os.path.join(temp_dir, egg_name)
    567             p = subprocess.Popen(['sudo', '/bin/sh', temp_egg],
    568                                  stdout=subprocess.PIPE)
    569             regex = re.compile('Copying (.*?) to (.*?)\n')
    570             match = regex.search(p.communicate()[0])
    571             status = p.wait()
    572 
    573             if match:
    574                 compiled = os.path.join(match.group(2), match.group(1))
    575                 os.system("sudo chmod a+r '%s'" % compiled)
    576         finally:
    577             shutil.rmtree(temp_dir)
    578 
    579         if status:
    580             logging.error('install of setuptools from egg failed.')
    581             return False
    582         return True
    583 
    584 
    585 class MySQLdbPackage(ExternalPackage):
    586     """mysql package, used in scheduler."""
    587     module_name = 'MySQLdb'
    588     version = '1.2.3'
    589     urls = ('http://downloads.sourceforge.net/project/mysql-python/'
    590             'mysql-python/%(version)s/MySQL-python-%(version)s.tar.gz'
    591             % dict(version=version),)
    592     local_filename = 'MySQL-python-%s.tar.gz' % version
    593     hex_sum = '3511bb8c57c6016eeafa531d5c3ea4b548915e3c'
    594 
    595     _build_and_install_current_dir = (
    596             ExternalPackage._build_and_install_current_dir_setup_py)
    597 
    598 
    599     def _build_and_install(self, install_dir):
    600         if not os.path.exists('/usr/bin/mysql_config'):
    601             error_msg = ('You need to install /usr/bin/mysql_config.\n'
    602                          'On Ubuntu or Debian based systems use this: '
    603                          'sudo apt-get install libmysqlclient15-dev')
    604             logging.error(error_msg)
    605             return False, error_msg
    606         return self._build_and_install_from_package(install_dir)
    607 
    608 
    609 class DjangoPackage(ExternalPackage):
    610     """django package."""
    611     version = '1.5.1'
    612     local_filename = 'Django-%s.tar.gz' % version
    613     urls = ('http://www.djangoproject.com/download/%s/tarball/' % version,)
    614     hex_sum = '0ab97b90c4c79636e56337f426f1e875faccbba1'
    615 
    616     _build_and_install = ExternalPackage._build_and_install_from_package
    617     _build_and_install_current_dir = (
    618             ExternalPackage._build_and_install_current_dir_noegg)
    619 
    620 
    621     def _get_installed_version_from_module(self, module):
    622         try:
    623             return module.get_version().split()[0]
    624         except AttributeError:
    625             return '0.9.6'
    626 
    627 
    628 
    629 class NumpyPackage(ExternalPackage):
    630     """numpy package, required by matploglib."""
    631     version = '1.7.0'
    632     local_filename = 'numpy-%s.tar.gz' % version
    633     urls = ('http://downloads.sourceforge.net/project/numpy/NumPy/%(version)s/'
    634             'numpy-%(version)s.tar.gz' % dict(version=version),)
    635     hex_sum = 'ba328985f20390b0f969a5be2a6e1141d5752cf9'
    636 
    637     _build_and_install = ExternalPackage._build_and_install_from_package
    638     _build_and_install_current_dir = (
    639             ExternalPackage._build_and_install_current_dir_setupegg_py)
    640 
    641 
    642 class MatplotlibPackage(ExternalPackage):
    643     """
    644     matplotlib package
    645 
    646     This requires numpy so it must be declared after numpy to guarantee that
    647     it is already installed.
    648     """
    649     version = '0.98.5.3'
    650     short_version = '0.98.5'
    651     local_filename = 'matplotlib-%s.tar.gz' % version
    652     urls = ('http://downloads.sourceforge.net/project/matplotlib/matplotlib/'
    653             'matplotlib-%s/matplotlib-%s.tar.gz' % (short_version, version),)
    654     hex_sum = '2f6c894cf407192b3b60351bcc6468c0385d47b6'
    655     os_requirements = {('/usr/include/freetype2/ft2build.h',
    656                         '/usr/include/ft2build.h'): 'libfreetype6-dev',
    657                        ('/usr/include/png.h'): 'libpng12-dev'}
    658 
    659     _build_and_install = ExternalPackage._build_and_install_from_package
    660     _build_and_install_current_dir = (
    661             ExternalPackage._build_and_install_current_dir_setupegg_py)
    662 
    663 
    664 class AtForkPackage(ExternalPackage):
    665     """atfork package"""
    666     version = '0.1.2'
    667     local_filename = 'atfork-%s.zip' % version
    668     urls = ('http://python-atfork.googlecode.com/files/' + local_filename,)
    669     hex_sum = '5baa64c73e966b57fa797040585c760c502dc70b'
    670 
    671     _build_and_install = ExternalPackage._build_and_install_from_package
    672     _build_and_install_current_dir = (
    673             ExternalPackage._build_and_install_current_dir_noegg)
    674 
    675 
    676 class ParamikoPackage(ExternalPackage):
    677     """paramiko package"""
    678     version = '1.7.5'
    679     local_filename = 'paramiko-%s.zip' % version
    680     urls = ('https://pypi.python.org/packages/source/p/paramiko/' + local_filename,)
    681     hex_sum = 'd23e437c0d8bd6aeb181d9990a9d670fb30d0c72'
    682 
    683 
    684     _build_and_install = ExternalPackage._build_and_install_from_package
    685 
    686 
    687     def _check_for_pycrypto(self):
    688         # NOTE(gps): Linux distros have better python-crypto packages than we
    689         # can easily get today via a wget due to the library's age and staleness
    690         # yet many security and behavior bugs are fixed by patches that distros
    691         # already apply.  PyCrypto has a new active maintainer in 2009.  Once a
    692         # new release is made (http://pycrypto.org/) we should add an installer.
    693         try:
    694             import Crypto
    695         except ImportError:
    696             logging.error('Please run "sudo apt-get install python-crypto" '
    697                           'or your Linux distro\'s equivalent.')
    698             return False
    699         return True
    700 
    701 
    702     def _build_and_install_current_dir(self, install_dir):
    703         if not self._check_for_pycrypto():
    704             return False
    705         # paramiko 1.7.4 doesn't require building, it is just a module directory
    706         # that we can rsync into place directly.
    707         if not os.path.isdir('paramiko'):
    708             raise Error('no paramiko directory in %s.' % os.getcwd())
    709         status = system("rsync -r 'paramiko' '%s/'" % install_dir)
    710         if status:
    711             logging.error('%s rsync to install_dir failed.', self.name)
    712             return False
    713         return True
    714 
    715 
    716 class RequestsPackage(ExternalPackage):
    717     """requests package"""
    718     version = '0.11.2'
    719     local_filename = 'requests-%s.tar.gz' % version
    720     urls = ('http://pypi.python.org/packages/source/r/requests/' +
    721             local_filename,)
    722     hex_sum = '00a49e8bd6dd8955acf6f6269d1b85f50c70b712'
    723 
    724     _build_and_install = ExternalPackage._build_and_install_from_package
    725     _build_and_install_current_dir = (
    726                         ExternalPackage._build_and_install_current_dir_setup_py)
    727 
    728 
    729 class JsonRPCLib(ExternalPackage):
    730     """jsonrpclib package"""
    731     version = '0.1.3'
    732     module_name = 'jsonrpclib'
    733     local_filename = '%s-%s.tar.gz' % (module_name, version)
    734     urls = ('http://pypi.python.org/packages/source/j/%s/%s' %
    735             (module_name, local_filename), )
    736     hex_sum = '431714ed19ab677f641ce5d678a6a95016f5c452'
    737 
    738     def _get_installed_version_from_module(self, module):
    739         # jsonrpclib doesn't contain a proper version
    740         return self.version
    741 
    742     _build_and_install = ExternalPackage._build_and_install_from_package
    743     _build_and_install_current_dir = (
    744                         ExternalPackage._build_and_install_current_dir_noegg)
    745 
    746 
    747 class Httplib2Package(ExternalPackage):
    748     """httplib2 package"""
    749     version = '0.6.0'
    750     local_filename = 'httplib2-%s.tar.gz' % version
    751     urls = ('http://httplib2.googlecode.com/files/' + local_filename,)
    752     hex_sum = '995344b2704826cc0d61a266e995b328d92445a5'
    753 
    754     def _get_installed_version_from_module(self, module):
    755         # httplib2 doesn't contain a proper version
    756         return self.version
    757 
    758     _build_and_install = ExternalPackage._build_and_install_from_package
    759     _build_and_install_current_dir = (
    760                         ExternalPackage._build_and_install_current_dir_noegg)
    761 
    762 
    763 class GwtPackage(ExternalPackage):
    764     """Fetch and extract a local copy of GWT used to build the frontend."""
    765 
    766     version = '2.3.0'
    767     local_filename = 'gwt-%s.zip' % version
    768     urls = ('http://google-web-toolkit.googlecode.com/files/' + local_filename,)
    769     hex_sum = 'd51fce9166e6b31349659ffca89baf93e39bc84b'
    770     name = 'gwt'
    771     about_filename = 'about.txt'
    772     module_name = None  # Not a Python module.
    773 
    774 
    775     def is_needed(self, install_dir):
    776         gwt_dir = os.path.join(install_dir, self.name)
    777         about_file = os.path.join(install_dir, self.name, self.about_filename)
    778 
    779         if not os.path.exists(gwt_dir) or not os.path.exists(about_file):
    780             logging.info('gwt not installed for autotest')
    781             return True
    782 
    783         f = open(about_file, 'r')
    784         version_line = f.readline()
    785         f.close()
    786 
    787         match = re.match(r'Google Web Toolkit (.*)', version_line)
    788         if not match:
    789             logging.info('did not find gwt version')
    790             return True
    791 
    792         logging.info('found gwt version %s', match.group(1))
    793         return match.group(1) != self.version
    794 
    795 
    796     def _build_and_install(self, install_dir):
    797         os.chdir(install_dir)
    798         self._extract_compressed_package()
    799         extracted_dir = self.local_filename[:-len('.zip')]
    800         target_dir = os.path.join(install_dir, self.name)
    801         if os.path.exists(target_dir):
    802             shutil.rmtree(target_dir)
    803         os.rename(extracted_dir, target_dir)
    804         return True
    805 
    806 
    807 class GVizAPIPackage(ExternalPackage):
    808     """gviz package"""
    809     module_name = 'gviz_api'
    810     version = '1.7.0'
    811     url_filename = 'gviz_api_py-%s.tar.gz' % version
    812     local_filename = 'google-visualization-python.tar.gz'
    813     urls = ('http://google-visualization-python.googlecode.com/files/%s' % (
    814         url_filename),)
    815     hex_sum = 'cd9a0fb4ca5c4f86c0d85756f501fd54ccf492d2'
    816 
    817     _build_and_install = ExternalPackage._build_and_install_from_package
    818     _build_and_install_current_dir = (
    819                         ExternalPackage._build_and_install_current_dir_noegg)
    820 
    821     def _get_installed_version_from_module(self, module):
    822         # gviz doesn't contain a proper version
    823         return self.version
    824 
    825 
    826 class StatsdPackage(ExternalPackage):
    827     """python-statsd package"""
    828     version = '1.7.2'
    829     url_filename = 'python-statsd-%s.tar.gz' % version
    830     local_filename = url_filename
    831     urls = ('http://pypi.python.org/packages/source/p/python-statsd/%s' % (
    832         url_filename),)
    833     hex_sum = '2cc186ebdb723e2420b432ab71639786d877694b'
    834 
    835     _build_and_install = ExternalPackage._build_and_install_from_package
    836     _build_and_install_current_dir = (
    837                         ExternalPackage._build_and_install_current_dir_setup_py)
    838 
    839 
    840 class GdataPackage(ExternalPackage):
    841     """
    842     Pulls the GData library, giving us an API to query tracker.
    843     """
    844 
    845     version = '2.0.14'
    846     url_filename = 'gdata-%s.tar.gz' % version
    847     local_filename = url_filename
    848     urls = ('http://gdata-python-client.googlecode.com/files/%s' % (
    849         url_filename),)
    850     hex_sum = '5eed0e01ab931e3f706ec544fc8f06ecac384e91'
    851 
    852     _build_and_install = ExternalPackage._build_and_install_from_package
    853     _build_and_install_current_dir = (
    854                         ExternalPackage._build_and_install_current_dir_noegg)
    855 
    856     def _get_installed_version_from_module(self, module):
    857         # gdata doesn't contain a proper version
    858         return self.version
    859 
    860 
    861 class GoogleAPIClientPackage(ExternalPackage):
    862     """
    863     Pulls the Python Google API client library.
    864     """
    865     version = '1.1'
    866     module_name = 'apiclient'
    867     url_filename = 'google-api-python-client-%s.tar.gz' % version
    868     local_filename = url_filename
    869     urls = ('https://google-api-python-client.googlecode.com/files/%s' % (
    870         url_filename),)
    871     hex_sum = '2294949683e367b3d4ecaeb77502509c5af21e60'
    872 
    873     _build_and_install = ExternalPackage._build_and_install_from_package
    874     _build_and_install_current_dir = (
    875                         ExternalPackage._build_and_install_current_dir_setup_py)
    876 
    877 
    878 class GFlagsPackage(ExternalPackage):
    879     """
    880     Gets the Python GFlags client library.
    881     """
    882     # gflags doesn't contain a proper version
    883     version = '2.0'
    884     url_filename = 'python-gflags-%s.tar.gz' % version
    885     local_filename = url_filename
    886     urls = ('https://python-gflags.googlecode.com/files/%s' % (
    887         url_filename),)
    888     hex_sum = 'db309e6964b102ff36de319ce551db512a78281e'
    889 
    890     _build_and_install = ExternalPackage._build_and_install_from_package
    891     _build_and_install_current_dir = (
    892                         ExternalPackage._build_and_install_current_dir_setup_py)
    893 
    894 
    895     def _get_installed_version_from_module(self, module):
    896         return self.version
    897 
    898 
    899 class DnsPythonPackage(ExternalPackage):
    900     """
    901     dns module
    902 
    903     Used in unittests.
    904     """
    905     module_name = 'dns'
    906     version = '1.3.5'
    907     url_filename = 'dnspython-%s.tar.gz' % version
    908     local_filename = url_filename
    909     urls = ('http://www.dnspython.org/kits/%s/%s' % (
    910         version, url_filename),)
    911 
    912     hex_sum = '06314dad339549613435470c6add992910e26e5d'
    913 
    914     _build_and_install = ExternalPackage._build_and_install_from_package
    915     _build_and_install_current_dir = (
    916                         ExternalPackage._build_and_install_current_dir_noegg)
    917 
    918     def _get_installed_version_from_module(self, module):
    919         """Ask our module its version string and return it or '' if unknown."""
    920         try:
    921             __import__(self.module_name + '.version')
    922             return module.version.version
    923         except AttributeError:
    924             logging.error('could not get version from %s', module)
    925             return ''
    926 
    927 
    928 class PyudevPackage(ExternalPackage):
    929     """
    930     pyudev module
    931 
    932     Used in unittests.
    933     """
    934     version = '0.16.1'
    935     url_filename = 'pyudev-%s.tar.gz' % version
    936     local_filename = url_filename
    937     urls = ('http://pypi.python.org/packages/source/p/pyudev/%s' % (
    938         url_filename),)
    939     hex_sum = 'b36bc5c553ce9b56d32a5e45063a2c88156771c0'
    940 
    941     _build_and_install = ExternalPackage._build_and_install_from_package
    942     _build_and_install_current_dir = (
    943                         ExternalPackage._build_and_install_current_dir_setup_py)
    944 
    945 
    946 class PyMoxPackage(ExternalPackage):
    947     """
    948     mox module
    949 
    950     Used in unittests.
    951     """
    952     module_name = 'mox'
    953     version = '0.5.3'
    954     url_filename = 'mox-%s.tar.gz' % version
    955     local_filename = url_filename
    956     urls = ('http://pypi.python.org/packages/source/m/mox/%s' % (
    957         url_filename),)
    958     hex_sum = '1c502d2c0a8aefbba2c7f385a83d33e7d822452a'
    959 
    960     _build_and_install = ExternalPackage._build_and_install_from_package
    961     _build_and_install_current_dir = (
    962                         ExternalPackage._build_and_install_current_dir_noegg)
    963 
    964     def _get_installed_version_from_module(self, module):
    965         # mox doesn't contain a proper version
    966         return self.version
    967 
    968 
    969 class PySeleniumPackage(ExternalPackage):
    970     """
    971     selenium module
    972 
    973     Used in wifi_interop suite.
    974     """
    975     module_name = 'selenium'
    976     version = '2.37.2'
    977     url_filename = 'selenium-%s.tar.gz' % version
    978     local_filename = url_filename
    979     urls = ('https://pypi.python.org/packages/source/s/selenium/%s' % (
    980         url_filename),)
    981     hex_sum = '66946d5349e36d946daaad625c83c30c11609e36'
    982 
    983     _build_and_install = ExternalPackage._build_and_install_from_package
    984     _build_and_install_current_dir = (
    985                         ExternalPackage._build_and_install_current_dir_setup_py)
    986 
    987 
    988 class FaultHandlerPackage(ExternalPackage):
    989     """
    990     faulthandler module
    991     """
    992     module_name = 'faulthandler'
    993     version = '2.3'
    994     url_filename = '%s-%s.tar.gz' % (module_name, version)
    995     local_filename = url_filename
    996     urls = ('http://pypi.python.org/packages/source/f/faulthandler/%s' %
    997             (url_filename),)
    998     hex_sum = 'efb30c068414fba9df892e48fcf86170cbf53589'
    999 
   1000     _build_and_install = ExternalPackage._build_and_install_from_package
   1001     _build_and_install_current_dir = (
   1002             ExternalPackage._build_and_install_current_dir_noegg)
   1003 
   1004 
   1005 class PsutilPackage(ExternalPackage):
   1006     """
   1007     psutil module
   1008     """
   1009     module_name = 'psutil'
   1010     version = '2.1.1'
   1011     url_filename = '%s-%s.tar.gz' % (module_name, version)
   1012     local_filename = url_filename
   1013     urls = ('http://pypi.python.org/packages/source/p/psutil/%s' %
   1014             (url_filename),)
   1015     hex_sum = '0c20a20ed316e69f2b0881530439213988229916'
   1016 
   1017     _build_and_install = ExternalPackage._build_and_install_from_package
   1018     _build_and_install_current_dir = (
   1019                         ExternalPackage._build_and_install_current_dir_setup_py)
   1020 
   1021 
   1022 class ElasticSearchPackage(ExternalPackage):
   1023     """elasticsearch-py package."""
   1024     version = '1.6.0'
   1025     url_filename = 'elasticsearch-%s.tar.gz' % version
   1026     local_filename = url_filename
   1027     urls = ('https://pypi.python.org/packages/source/e/elasticsearch/%s' %
   1028             (url_filename),)
   1029     hex_sum = '3e676c96f47935b1f52df82df3969564bd356b1c'
   1030     _build_and_install = ExternalPackage._build_and_install_from_package
   1031     _build_and_install_current_dir = (
   1032             ExternalPackage._build_and_install_current_dir_setup_py)
   1033 
   1034 
   1035 class Urllib3Package(ExternalPackage):
   1036     """elasticsearch-py package."""
   1037     version = '1.9'
   1038     url_filename = 'urllib3-%s.tar.gz' % version
   1039     local_filename = url_filename
   1040     urls = ('https://pypi.python.org/packages/source/u/urllib3/%s' %
   1041             (url_filename),)
   1042     hex_sum = '9522197efb2a2b49ce804de3a515f06d97b6602f'
   1043     _build_and_install = ExternalPackage._build_and_install_from_package
   1044     _build_and_install_current_dir = (
   1045             ExternalPackage._build_and_install_current_dir_setup_py)
   1046 
   1047 
   1048 class ImagingLibraryPackage(ExternalPackage):
   1049     """Python Imaging Library (PIL)."""
   1050     version = '1.1.7'
   1051     url_filename = 'Imaging-%s.tar.gz' % version
   1052     local_filename = url_filename
   1053     urls = ('http://effbot.org/downloads/%s' % url_filename,)
   1054     hex_sum = '76c37504251171fda8da8e63ecb8bc42a69a5c81'
   1055 
   1056     def _build_and_install(self, install_dir):
   1057         # The path of zlib library might be different from what PIL setup.py is
   1058         # expected. Following change does the best attempt to link the library
   1059         # to a path PIL setup.py will try.
   1060         libz_possible_path = '/usr/lib/x86_64-linux-gnu/libz.so'
   1061         libz_expected_path = '/usr/lib/libz.so'
   1062         if (os.path.exists(libz_possible_path) and
   1063             not os.path.exists(libz_expected_path)):
   1064             utils.run('sudo ln -s %s %s' %
   1065                       (libz_possible_path, libz_expected_path))
   1066         return self._build_and_install_from_package(install_dir)
   1067 
   1068     _build_and_install_current_dir = (
   1069             ExternalPackage._build_and_install_current_dir_noegg)
   1070 
   1071 
   1072 class _ExternalGitRepo(ExternalPackage):
   1073     """
   1074     Parent class for any package which needs to pull a git repo.
   1075 
   1076     This class inherits from ExternalPackage only so we can sync git
   1077     repos through the build_externals script. We do not reuse any of
   1078     ExternalPackage's other methods. Any package that needs a git repo
   1079     should subclass this and override build_and_install or fetch as
   1080     they see appropriate.
   1081     """
   1082 
   1083     os_requirements = {('/usr/bin/git') : 'git-core'}
   1084 
   1085     # All the chromiumos projects used on the lab servers should have a 'prod'
   1086     # branch used to track the software version deployed in prod.
   1087     PROD_BRANCH = 'prod'
   1088 
   1089     def is_needed(self, unused_install_dir):
   1090         """Tell build_externals that we need to fetch."""
   1091         # TODO(beeps): check if we're already upto date.
   1092         return True
   1093 
   1094 
   1095     def build_and_install(self, unused_install_dir):
   1096         """
   1097         Fall through method to install a package.
   1098 
   1099         Overwritten in base classes to pull a git repo.
   1100         """
   1101         raise NotImplementedError
   1102 
   1103 
   1104     def fetch(self, unused_dest_dir):
   1105         """Fallthrough method to fetch a package."""
   1106         return True
   1107 
   1108 
   1109 class HdctoolsRepo(_ExternalGitRepo):
   1110     """Clones or updates the hdctools repo."""
   1111 
   1112     module_name = 'servo'
   1113     temp_hdctools_dir = tempfile.mktemp(suffix='hdctools')
   1114     _GIT_URL = ('https://chromium.googlesource.com/'
   1115                 'chromiumos/third_party/hdctools')
   1116 
   1117     def fetch(self, unused_dest_dir):
   1118         """
   1119         Fetch repo to a temporary location.
   1120 
   1121         We use an intermediate temp directory to stage our
   1122         installation because we only care about the servo package.
   1123         If we can't get at the top commit hash after fetching
   1124         something is wrong. This can happen when we've cloned/pulled
   1125         an empty repo. Not something we expect to do.
   1126 
   1127         @parma unused_dest_dir: passed in because we inherit from
   1128             ExternalPackage.
   1129 
   1130         @return: True if repo sync was successful.
   1131         """
   1132         git_repo = revision_control.GitRepo(
   1133                         self.temp_hdctools_dir,
   1134                         self._GIT_URL,
   1135                         None,
   1136                         abs_work_tree=self.temp_hdctools_dir)
   1137         git_repo.reinit_repo_at(self.PROD_BRANCH)
   1138 
   1139         if git_repo.get_latest_commit_hash():
   1140             return True
   1141         return False
   1142 
   1143 
   1144     def build_and_install(self, install_dir):
   1145         """Reach into the hdctools repo and rsync only the servo directory."""
   1146 
   1147         servo_dir = os.path.join(self.temp_hdctools_dir, 'servo')
   1148         if not os.path.exists(servo_dir):
   1149             return False
   1150 
   1151         rv = self._rsync(servo_dir, os.path.join(install_dir, 'servo'))
   1152         shutil.rmtree(self.temp_hdctools_dir)
   1153         return rv
   1154 
   1155 
   1156 class ChromiteRepo(_ExternalGitRepo):
   1157     """Clones or updates the chromite repo."""
   1158 
   1159     _GIT_URL = ('https://chromium.googlesource.com/chromiumos/chromite')
   1160 
   1161     def build_and_install(self, install_dir):
   1162         """
   1163         Clone if the repo isn't initialized, pull clean bits if it is.
   1164 
   1165         Unlike it's hdctools counterpart the chromite repo clones master
   1166         directly into site-packages. It doesn't use an intermediate temp
   1167         directory because it doesn't need installation.
   1168 
   1169         @param install_dir: destination directory for chromite installation.
   1170         """
   1171         local_chromite_dir = os.path.join(install_dir, 'chromite')
   1172         git_repo = revision_control.GitRepo(
   1173                 local_chromite_dir,
   1174                 self._GIT_URL,
   1175                 abs_work_tree=local_chromite_dir)
   1176         git_repo.reinit_repo_at(self.PROD_BRANCH)
   1177 
   1178 
   1179         if git_repo.get_latest_commit_hash():
   1180             return True
   1181         return False
   1182 
   1183 
   1184 class DevServerRepo(_ExternalGitRepo):
   1185     """Clones or updates the chromite repo."""
   1186 
   1187     _GIT_URL = ('https://chromium.googlesource.com/'
   1188                 'chromiumos/platform/dev-util')
   1189 
   1190     def build_and_install(self, install_dir):
   1191         """
   1192         Clone if the repo isn't initialized, pull clean bits if it is.
   1193 
   1194         Unlike it's hdctools counterpart the dev-util repo clones master
   1195         directly into site-packages. It doesn't use an intermediate temp
   1196         directory because it doesn't need installation.
   1197 
   1198         @param install_dir: destination directory for chromite installation.
   1199         """
   1200         local_devserver_dir = os.path.join(install_dir, 'devserver')
   1201         git_repo = revision_control.GitRepo(local_devserver_dir, self._GIT_URL,
   1202                                             abs_work_tree=local_devserver_dir)
   1203         git_repo.reinit_repo_at(self.PROD_BRANCH)
   1204 
   1205         if git_repo.get_latest_commit_hash():
   1206             return True
   1207         return False
   1208 
   1209 
   1210 class BtsocketRepo(_ExternalGitRepo):
   1211     """Clones or updates the btsocket repo."""
   1212 
   1213     _GIT_URL = ('https://chromium.googlesource.com/'
   1214                 'chromiumos/platform/btsocket')
   1215 
   1216     def fetch(self, unused_dest_dir):
   1217         """
   1218         Fetch repo to a temporary location.
   1219 
   1220         We use an intermediate temp directory because we have to build an
   1221         egg for installation.  If we can't get at the top commit hash after
   1222         fetching something is wrong. This can happen when we've cloned/pulled
   1223         an empty repo. Not something we expect to do.
   1224 
   1225         @parma unused_dest_dir: passed in because we inherit from
   1226             ExternalPackage.
   1227 
   1228         @return: True if repo sync was successful.
   1229         """
   1230         self.temp_btsocket_dir = autotemp.tempdir(unique_id='btsocket')
   1231         try:
   1232             git_repo = revision_control.GitRepo(
   1233                             self.temp_btsocket_dir.name,
   1234                             self._GIT_URL,
   1235                             None,
   1236                             abs_work_tree=self.temp_btsocket_dir.name)
   1237             git_repo.reinit_repo_at(self.PROD_BRANCH)
   1238 
   1239             if git_repo.get_latest_commit_hash():
   1240                 return True
   1241         except:
   1242             self.temp_btsocket_dir.clean()
   1243             raise
   1244 
   1245         self.temp_btsocket_dir.clean()
   1246         return False
   1247 
   1248 
   1249     def build_and_install(self, install_dir):
   1250         """
   1251         Install the btsocket module using setup.py
   1252 
   1253         @param install_dir: Target installation directory.
   1254 
   1255         @return: A boolean indicating success of failure.
   1256         """
   1257         work_dir = os.getcwd()
   1258         try:
   1259             os.chdir(self.temp_btsocket_dir.name)
   1260             rv = self._build_and_install_current_dir_setup_py(install_dir)
   1261         finally:
   1262             os.chdir(work_dir)
   1263             self.temp_btsocket_dir.clean()
   1264         return rv
   1265