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