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