external_packages.py revision d6a4718a15d37306f31a65ad8d6b9023b4a69b42
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 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    logging.info(commandline)
31    return os.system(commandline)
32
33
34def find_top_of_autotest_tree():
35    """@returns The full path to the top of the autotest directory tree."""
36    dirname = os.path.dirname(__file__)
37    autotest_dir = os.path.abspath(os.path.join(dirname, '..'))
38    return autotest_dir
39
40
41class ExternalPackage(object):
42    """
43    Defines an external package with URLs to fetch its sources from and
44    a build_and_install() method to unpack it, build it and install it
45    beneath our own autotest/site-packages directory.
46
47    Base Class.  Subclass this to define packages.
48
49    Attributes:
50      @attribute urls - A tuple of URLs to try fetching the package from.
51      @attribute local_filename - A local filename to use when saving the
52              fetched package.
53      @attribute hex_sum - The hex digest (currently SHA1) of this package
54              to be used to verify its contents.
55      @attribute module_name - The installed python module name to be used for
56              for a version check.  Defaults to the lower case class name with
57              the word Package stripped off.
58      @attribute version - The desired minimum package version.
59      @attribute os_requirements - A dictionary mapping a file pathname on the
60              the OS distribution to a likely name of a package the user
61              needs to install on their system in order to get this file.
62      @attribute name - Read only, the printable name of the package.
63      @attribute subclasses - This class attribute holds a list of all defined
64              subclasses.  It is constructed dynamically using the metaclass.
65    """
66    subclasses = []
67    urls = ()
68    local_filename = None
69    hex_sum = None
70    module_name = None
71    version = None
72    os_requirements = None
73
74
75    class __metaclass__(type):
76        """Any time a subclass is defined, add it to our list."""
77        def __init__(mcs, name, bases, dict):
78            if name != 'ExternalPackage':
79                mcs.subclasses.append(mcs)
80
81
82    def __init__(self):
83        self.verified_package = ''
84        if not self.module_name:
85            self.module_name = self.name.lower()
86        self.installed_version = ''
87
88
89    @property
90    def name(self):
91        """Return the class name with any trailing 'Package' stripped off."""
92        class_name = self.__class__.__name__
93        if class_name.endswith('Package'):
94            return class_name[:-len('Package')]
95        return class_name
96
97
98    def is_needed(self, unused_install_dir):
99        """@returns True if self.module_name needs to be built and installed."""
100        if not self.module_name or not self.version:
101            logging.warning('version and module_name required for '
102                            'is_needed() check to work.')
103            return True
104        try:
105            module = __import__(self.module_name)
106        except ImportError, e:
107            logging.info("%s isn't present. Will install.", self.module_name)
108            return True
109        self.installed_version = self._get_installed_version_from_module(module)
110        logging.info('imported %s version %s.', self.module_name,
111                     self.installed_version)
112        if hasattr(self, 'minimum_version'):
113            return self.minimum_version > self.installed_version
114        else:
115            return self.version > self.installed_version
116
117
118    def _get_installed_version_from_module(self, module):
119        """Ask our module its version string and return it or '' if unknown."""
120        try:
121            return module.__version__
122        except AttributeError:
123            logging.error('could not get version from %s', module)
124            return ''
125
126
127    def _build_and_install(self, install_dir):
128        """Subclasses MUST provide their own implementation."""
129        raise NotImplementedError
130
131
132    def _build_and_install_current_dir(self, install_dir):
133        """
134        Subclasses that use _build_and_install_from_package() MUST provide
135        their own implementation of this method.
136        """
137        raise NotImplementedError
138
139
140    def build_and_install(self, install_dir):
141        """
142        Builds and installs the package.  It must have been fetched already.
143
144        @param install_dir - The package installation directory.  If it does
145            not exist it will be created.
146        """
147        if not self.verified_package:
148            raise Error('Must call fetch() first.  - %s' % self.name)
149        self._check_os_requirements()
150        return self._build_and_install(install_dir)
151
152
153    def _check_os_requirements(self):
154        if not self.os_requirements:
155            return
156        failed = False
157        for file_name, package_name in self.os_requirements.iteritems():
158            if not os.path.exists(file_name):
159                failed = True
160                logging.error('File %s not found, %s needs it.',
161                              file_name, self.name)
162                logging.error('Perhaps you need to install something similar '
163                              'to the %s package for OS first.', package_name)
164        if failed:
165            raise Error('Missing OS requirements for %s.  (see above)' %
166                        self.name)
167
168
169    def _build_and_install_current_dir_setup_py(self, install_dir):
170        """For use as a _build_and_install_current_dir implementation."""
171        egg_path = self._build_egg_using_setup_py(setup_py='setup.py')
172        if not egg_path:
173            return False
174        return self._install_from_egg(install_dir, egg_path)
175
176
177    def _build_and_install_current_dir_setupegg_py(self, install_dir):
178        """For use as a _build_and_install_current_dir implementation."""
179        egg_path = self._build_egg_using_setup_py(setup_py='setupegg.py')
180        if not egg_path:
181            return False
182        return self._install_from_egg(install_dir, egg_path)
183
184
185    def _build_and_install_current_dir_noegg(self, install_dir):
186        if not self._build_using_setup_py():
187            return False
188        return self._install_using_setup_py_and_rsync(install_dir)
189
190
191    def _build_and_install_from_package(self, install_dir):
192        """
193        This method may be used as a _build_and_install() implementation
194        for subclasses if they implement _build_and_install_current_dir().
195
196        Extracts the .tar.gz file, chdirs into the extracted directory
197        (which is assumed to match the tar filename) and calls
198        _build_and_isntall_current_dir from there.
199
200        Afterwards the build (regardless of failure) extracted .tar.gz
201        directory is cleaned up.
202
203        @returns True on success, False otherwise.
204
205        @raises OSError If the expected extraction directory does not exist.
206        """
207        self._extract_compressed_package()
208        if self.verified_package.endswith('.tar.gz'):
209            extension = '.tar.gz'
210        elif self.verified_package.endswith('.tar.bz2'):
211            extension = '.tar.bz2'
212        elif self.verified_package.endswith('.zip'):
213            extension = '.zip'
214        else:
215            raise Error('Unexpected package file extension on %s' %
216                        self.verified_package)
217        os.chdir(os.path.dirname(self.verified_package))
218        os.chdir(self.local_filename[:-len(extension)])
219        extracted_dir = os.getcwd()
220        try:
221            return self._build_and_install_current_dir(install_dir)
222        finally:
223            os.chdir(os.path.join(extracted_dir, '..'))
224            shutil.rmtree(extracted_dir)
225
226
227    def _extract_compressed_package(self):
228        """Extract the fetched compressed .tar or .zip within its directory."""
229        if not self.verified_package:
230            raise Error('Package must have been fetched first.')
231        os.chdir(os.path.dirname(self.verified_package))
232        if self.verified_package.endswith('gz'):
233            status = system("tar -xzf '%s'" % self.verified_package)
234        elif self.verified_package.endswith('bz2'):
235            status = system("tar -xjf '%s'" % self.verified_package)
236        elif self.verified_package.endswith('zip'):
237            status = system("unzip '%s'" % self.verified_package)
238        else:
239            raise Error('Unknown compression suffix on %s.' %
240                        self.verified_package)
241        if status:
242            raise Error('tar failed with %s' % (status,))
243
244
245    def _build_using_setup_py(self, setup_py='setup.py'):
246        """
247        Assuming the cwd is the extracted python package, execute a simple
248        python setup.py build.
249
250        @param setup_py - The name of the setup.py file to execute.
251
252        @returns True on success, False otherwise.
253        """
254        if not os.path.exists(setup_py):
255            raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
256        status = system("'%s' %s build" % (sys.executable, setup_py))
257        if status:
258            logging.error('%s build failed.' % self.name)
259            return False
260        return True
261
262
263    def _build_egg_using_setup_py(self, setup_py='setup.py'):
264        """
265        Assuming the cwd is the extracted python package, execute a simple
266        python setup.py bdist_egg.
267
268        @param setup_py - The name of the setup.py file to execute.
269
270        @returns The relative path to the resulting egg file or '' on failure.
271        """
272        if not os.path.exists(setup_py):
273            raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
274        egg_subdir = 'dist'
275        if os.path.isdir(egg_subdir):
276            shutil.rmtree(egg_subdir)
277        status = system("'%s' %s bdist_egg" % (sys.executable, setup_py))
278        if status:
279            logging.error('bdist_egg of setuptools failed.')
280            return ''
281        # I've never seen a bdist_egg lay multiple .egg files.
282        for filename in os.listdir(egg_subdir):
283            if filename.endswith('.egg'):
284                return os.path.join(egg_subdir, filename)
285
286
287    def _install_from_egg(self, install_dir, egg_path):
288        """
289        Install a module from an egg file by unzipping the necessary parts
290        into install_dir.
291
292        @param install_dir - The installation directory.
293        @param egg_path - The pathname of the egg file.
294        """
295        status = system("unzip -q -o -d '%s' '%s'" % (install_dir, egg_path))
296        if status:
297            logging.error('unzip of %s failed', egg_path)
298            return False
299        egg_info = os.path.join(install_dir, 'EGG-INFO')
300        if os.path.isdir(egg_info):
301            shutil.rmtree(egg_info)
302        return True
303
304
305    def _get_temp_dir(self):
306        return tempfile.mkdtemp(dir='/var/tmp')
307
308
309    def _site_packages_path(self, temp_dir):
310        # This makes assumptions about what python setup.py install
311        # does when given a prefix.  Is this always correct?
312        python_xy = 'python%s' % sys.version[:3]
313        return os.path.join(temp_dir, 'lib', python_xy, 'site-packages')
314
315
316    def _install_using_setup_py_and_rsync(self, install_dir,
317                                          setup_py='setup.py',
318                                          temp_dir=None):
319        """
320        Assuming the cwd is the extracted python package, execute a simple:
321
322          python setup.py install --prefix=BLA
323
324        BLA will be a temporary directory that everything installed will
325        be picked out of and rsynced to the appropriate place under
326        install_dir afterwards.
327
328        Afterwards, it deconstructs the extra lib/pythonX.Y/site-packages/
329        directory tree that setuptools created and moves all installed
330        site-packages directly up into install_dir itself.
331
332        @param install_dir the directory for the install to happen under.
333        @param setup_py - The name of the setup.py file to execute.
334
335        @returns True on success, False otherwise.
336        """
337        if not os.path.exists(setup_py):
338            raise Error('%s does not exist in %s' % (setup_py, os.getcwd()))
339
340        if temp_dir is None:
341            temp_dir = self._get_temp_dir()
342
343        try:
344            status = system("'%s' %s install --no-compile --prefix='%s'"
345                            % (sys.executable, setup_py, temp_dir))
346            if status:
347                logging.error('%s install failed.' % self.name)
348                return False
349
350            if os.path.isdir(os.path.join(temp_dir, 'lib')):
351                # NOTE: This ignores anything outside of the lib/ dir that
352                # was installed.
353                temp_site_dir = self._site_packages_path(temp_dir)
354            else:
355                temp_site_dir = temp_dir
356
357            status = system("rsync -r '%s/' '%s/'" %
358                            (temp_site_dir, install_dir))
359            if status:
360                logging.error('%s rsync to install_dir failed.' % self.name)
361                return False
362            return True
363        finally:
364            shutil.rmtree(temp_dir)
365
366
367
368    def _build_using_make(self, install_dir):
369        """Build the current package using configure/make.
370
371        @returns True on success, False otherwise.
372        """
373        install_prefix = os.path.join(install_dir, 'usr', 'local')
374        status = system('./configure --prefix=%s' % install_prefix)
375        if status:
376            logging.error('./configure failed for %s', self.name)
377            return False
378        status = system('make')
379        if status:
380            logging.error('make failed for %s', self.name)
381            return False
382        status = system('make check')
383        if status:
384            logging.error('make check failed for %s', self.name)
385            return False
386        return True
387
388
389    def _install_using_make(self):
390        """Install the current package using make install.
391
392        Assumes the install path was set up while running ./configure (in
393        _build_using_make()).
394
395        @returns True on success, False otherwise.
396        """
397        status = system('make install')
398        return status == 0
399
400
401    def fetch(self, dest_dir):
402        """
403        Fetch the package from one its URLs and save it in dest_dir.
404
405        If the the package already exists in dest_dir and the checksum
406        matches this code will not fetch it again.
407
408        Sets the 'verified_package' attribute with the destination pathname.
409
410        @param dest_dir - The destination directory to save the local file.
411            If it does not exist it will be created.
412
413        @returns A boolean indicating if we the package is now in dest_dir.
414        @raises FetchError - When something unexpected happens.
415        """
416        if not os.path.exists(dest_dir):
417            os.makedirs(dest_dir)
418        local_path = os.path.join(dest_dir, self.local_filename)
419
420        # If the package exists, verify its checksum and be happy if it is good.
421        if os.path.exists(local_path):
422            actual_hex_sum = _checksum_file(local_path)
423            if self.hex_sum == actual_hex_sum:
424                logging.info('Good checksum for existing %s package.',
425                             self.name)
426                self.verified_package = local_path
427                return True
428            logging.warning('Bad checksum for existing %s package.  '
429                            'Re-downloading', self.name)
430            os.rename(local_path, local_path + '.wrong-checksum')
431
432        # Download the package from one of its urls, rejecting any if the
433        # checksum does not match.
434        for url in self.urls:
435            logging.info('Fetching %s', url)
436            try:
437                url_file = urllib2.urlopen(url)
438            except (urllib2.URLError, EnvironmentError):
439                logging.warning('Could not fetch %s package from %s.',
440                                self.name, url)
441                continue
442            data_length = int(url_file.info().get('Content-Length',
443                                                  _MAX_PACKAGE_SIZE))
444            if data_length <= 0 or data_length > _MAX_PACKAGE_SIZE:
445                raise FetchError('%s from %s fails Content-Length %d '
446                                 'sanity check.' % (self.name, url,
447                                                    data_length))
448            checksum = utils.hash('sha1')
449            total_read = 0
450            output = open(local_path, 'wb')
451            try:
452                while total_read < data_length:
453                    data = url_file.read(_READ_SIZE)
454                    if not data:
455                        break
456                    output.write(data)
457                    checksum.update(data)
458                    total_read += len(data)
459            finally:
460                output.close()
461            if self.hex_sum != checksum.hexdigest():
462                logging.warning('Bad checksum for %s fetched from %s.',
463                                self.name, url)
464                logging.warning('Got %s', checksum.hexdigest())
465                logging.warning('Expected %s', self.hex_sum)
466                os.unlink(local_path)
467                continue
468            logging.info('Good checksum.')
469            self.verified_package = local_path
470            return True
471        else:
472            return False
473
474
475# NOTE: This class definition must come -before- all other ExternalPackage
476# classes that need to use this version of setuptools so that is is inserted
477# into the ExternalPackage.subclasses list before them.
478class SetuptoolsPackage(ExternalPackage):
479    # For all known setuptools releases a string compare works for the
480    # version string.  Hopefully they never release a 0.10.  (Their own
481    # version comparison code would break if they did.)
482    # Any system with setuptools > 0.6 is fine. If none installed, then
483    # try to install the latest found on the upstream.
484    minimum_version = '0.6'
485    version = '0.6c11'
486    urls = ('http://pypi.python.org/packages/source/s/setuptools/'
487            'setuptools-%s.tar.gz' % (version,),)
488    local_filename = 'setuptools-%s.tar.gz' % version
489    hex_sum = '8d1ad6384d358c547c50c60f1bfdb3362c6c4a7d'
490
491    SUDO_SLEEP_DELAY = 15
492
493
494    def _build_and_install(self, install_dir):
495        """Install setuptools on the system."""
496        logging.info('NOTE: setuptools install does not use install_dir.')
497        return self._build_and_install_from_package(install_dir)
498
499
500    def _build_and_install_current_dir(self, install_dir):
501        egg_path = self._build_egg_using_setup_py()
502        if not egg_path:
503            return False
504
505        print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n'
506        print 'About to run sudo to install setuptools', self.version
507        print 'on your system for use by', sys.executable, '\n'
508        print '!! ^C within', self.SUDO_SLEEP_DELAY, 'seconds to abort.\n'
509        time.sleep(self.SUDO_SLEEP_DELAY)
510
511        # Copy the egg to the local filesystem /var/tmp so that root can
512        # access it properly (avoid NFS squashroot issues).
513        temp_dir = self._get_temp_dir()
514        try:
515            shutil.copy(egg_path, temp_dir)
516            egg_name = os.path.split(egg_path)[1]
517            temp_egg = os.path.join(temp_dir, egg_name)
518            p = subprocess.Popen(['sudo', '/bin/sh', temp_egg],
519                                 stdout=subprocess.PIPE)
520            regex = re.compile('Copying (.*?) to (.*?)\n')
521            match = regex.search(p.communicate()[0])
522            status = p.wait()
523
524            if match:
525                compiled = os.path.join(match.group(2), match.group(1))
526                os.system("sudo chmod a+r '%s'" % compiled)
527        finally:
528            shutil.rmtree(temp_dir)
529
530        if status:
531            logging.error('install of setuptools from egg failed.')
532            return False
533        return True
534
535
536class MySQLdbPackage(ExternalPackage):
537    module_name = 'MySQLdb'
538    version = '1.2.2'
539    urls = ('http://downloads.sourceforge.net/project/mysql-python/'
540            'mysql-python/%(version)s/MySQL-python-%(version)s.tar.gz'
541            % dict(version=version),)
542    local_filename = 'MySQL-python-%s.tar.gz' % version
543    hex_sum = '945a04773f30091ad81743f9eb0329a3ee3de383'
544
545    _build_and_install_current_dir = (
546            ExternalPackage._build_and_install_current_dir_setup_py)
547
548
549    def _build_and_install(self, install_dir):
550        if not os.path.exists('/usr/bin/mysql_config'):
551            logging.error('You need to install /usr/bin/mysql_config')
552            logging.error('On Ubuntu or Debian based systems use this: '
553                          'sudo apt-get install libmysqlclient15-dev')
554            return False
555        return self._build_and_install_from_package(install_dir)
556
557
558class DjangoPackage(ExternalPackage):
559    version = '1.3'
560    local_filename = 'Django-%s.tar.gz' % version
561    urls = ('http://www.djangoproject.com/download/%s/tarball/' % version,)
562    hex_sum = 'f8814d5e1412bb932318db5130260da5bf053ff7'
563
564    _build_and_install = ExternalPackage._build_and_install_from_package
565    _build_and_install_current_dir = (
566            ExternalPackage._build_and_install_current_dir_noegg)
567
568
569    def _get_installed_version_from_module(self, module):
570        try:
571            return module.get_version().split()[0]
572        except AttributeError:
573            return '0.9.6'
574
575
576
577class NumpyPackage(ExternalPackage):
578    version = '1.2.1'
579    local_filename = 'numpy-%s.tar.gz' % version
580    urls = ('http://downloads.sourceforge.net/project/numpy/NumPy/%(version)s/'
581            'numpy-%(version)s.tar.gz' % dict(version=version),)
582    hex_sum = '1aa706e733aea18eaffa70d93c0105718acb66c5'
583
584    _build_and_install = ExternalPackage._build_and_install_from_package
585    _build_and_install_current_dir = (
586            ExternalPackage._build_and_install_current_dir_setupegg_py)
587
588
589# This requires numpy so it must be declared after numpy to guarantee that it
590# is already installed.
591class MatplotlibPackage(ExternalPackage):
592    version = '0.98.5.3'
593    short_version = '0.98.5'
594    local_filename = 'matplotlib-%s.tar.gz' % version
595    urls = ('http://downloads.sourceforge.net/project/matplotlib/matplotlib/'
596            'matplotlib-%s/matplotlib-%s.tar.gz' % (short_version, version),)
597    hex_sum = '2f6c894cf407192b3b60351bcc6468c0385d47b6'
598    os_requirements = {'/usr/include/ft2build.h': 'libfreetype6-dev',
599                       '/usr/include/png.h': 'libpng12-dev'}
600
601    _build_and_install = ExternalPackage._build_and_install_from_package
602    _build_and_install_current_dir = (
603            ExternalPackage._build_and_install_current_dir_setupegg_py)
604
605
606class AtForkPackage(ExternalPackage):
607    version = '0.1.2'
608    local_filename = 'atfork-%s.zip' % version
609    urls = ('http://python-atfork.googlecode.com/files/' + local_filename,)
610    hex_sum = '5baa64c73e966b57fa797040585c760c502dc70b'
611
612    _build_and_install = ExternalPackage._build_and_install_from_package
613    _build_and_install_current_dir = (
614            ExternalPackage._build_and_install_current_dir_noegg)
615
616
617class ParamikoPackage(ExternalPackage):
618    version = '1.7.5'
619    local_filename = 'paramiko-%s.tar.gz' % version
620    urls = ('http://www.lag.net/paramiko/download/' + local_filename,
621            'ftp://mirrors.kernel.org/gentoo/distfiles/' + local_filename,)
622    hex_sum = '592be7a08290070b71da63a8e6f28a803399e5c5'
623
624
625    _build_and_install = ExternalPackage._build_and_install_from_package
626
627
628    def _check_for_pycrypto(self):
629        # NOTE(gps): Linux distros have better python-crypto packages than we
630        # can easily get today via a wget due to the library's age and staleness
631        # yet many security and behavior bugs are fixed by patches that distros
632        # already apply.  PyCrypto has a new active maintainer in 2009.  Once a
633        # new release is made (http://pycrypto.org/) we should add an installer.
634        try:
635            import Crypto
636        except ImportError:
637            logging.error('Please run "sudo apt-get install python-crypto" '
638                          'or your Linux distro\'s equivalent.')
639            return False
640        return True
641
642
643    def _build_and_install_current_dir(self, install_dir):
644        if not self._check_for_pycrypto():
645            return False
646        # paramiko 1.7.4 doesn't require building, it is just a module directory
647        # that we can rsync into place directly.
648        if not os.path.isdir('paramiko'):
649            raise Error('no paramiko directory in %s.' % os.getcwd())
650        status = system("rsync -r 'paramiko' '%s/'" % install_dir)
651        if status:
652            logging.error('%s rsync to install_dir failed.' % self.name)
653            return False
654        return True
655
656
657class RequestsPackage(ExternalPackage):
658    version = '0.11.2'
659    local_filename = 'requests-%s.tar.gz' % version
660    urls = ('http://pypi.python.org/packages/source/r/requests/' +
661            local_filename,)
662    hex_sum = '00a49e8bd6dd8955acf6f6269d1b85f50c70b712'
663
664    _build_and_install = ExternalPackage._build_and_install_from_package
665    _build_and_install_current_dir = (
666                        ExternalPackage._build_and_install_current_dir_setup_py)
667
668
669class SimplejsonPackage(ExternalPackage):
670    version = '2.0.9'
671    local_filename = 'simplejson-%s.tar.gz' % version
672    urls = ('http://pypi.python.org/packages/source/s/simplejson/' +
673            local_filename,)
674    hex_sum = 'b5b26059adbe677b06c299bed30557fcb0c7df8c'
675
676    _build_and_install = ExternalPackage._build_and_install_from_package
677    _build_and_install_current_dir = (
678                        ExternalPackage._build_and_install_current_dir_setup_py)
679
680
681class Httplib2Package(ExternalPackage):
682    version = '0.6.0'
683    local_filename = 'httplib2-%s.tar.gz' % version
684    urls = ('http://httplib2.googlecode.com/files/' + local_filename,)
685    hex_sum = '995344b2704826cc0d61a266e995b328d92445a5'
686
687    def _get_installed_version_from_module(self, module):
688        # httplib2 doesn't contain a proper version
689        return self.version
690
691    _build_and_install = ExternalPackage._build_and_install_from_package
692    _build_and_install_current_dir = (
693                        ExternalPackage._build_and_install_current_dir_noegg)
694
695
696class GwtPackage(ExternalPackage):
697    """Fetch and extract a local copy of GWT used to build the frontend."""
698
699    version = '2.3.0'
700    local_filename = 'gwt-%s.zip' % version
701    urls = ('http://google-web-toolkit.googlecode.com/files/' + local_filename,)
702    hex_sum = 'd51fce9166e6b31349659ffca89baf93e39bc84b'
703    name = 'gwt'
704    about_filename = 'about.txt'
705    module_name = None  # Not a Python module.
706
707
708    def is_needed(self, install_dir):
709        gwt_dir = os.path.join(install_dir, self.name)
710        about_file = os.path.join(install_dir, self.name, self.about_filename)
711
712        if not os.path.exists(gwt_dir) or not os.path.exists(about_file):
713            logging.info('gwt not installed for autotest')
714            return True
715
716        f = open(about_file, 'r')
717        version_line = f.readline()
718        f.close()
719
720        match = re.match(r'Google Web Toolkit (.*)', version_line)
721        if not match:
722            logging.info('did not find gwt version')
723            return True
724
725        logging.info('found gwt version %s', match.group(1))
726        return match.group(1) != self.version
727
728
729    def _build_and_install(self, install_dir):
730        os.chdir(install_dir)
731        self._extract_compressed_package()
732        extracted_dir = self.local_filename[:-len('.zip')]
733        target_dir = os.path.join(install_dir, self.name)
734        if os.path.exists(target_dir):
735            shutil.rmtree(target_dir)
736        os.rename(extracted_dir, target_dir)
737        return True
738
739
740class GVizAPIPackage(ExternalPackage):
741    version = '1.7.0'
742    url_filename = 'gviz_api_py-%s.tar.gz' % version
743    local_filename = 'google-visualization-python.tar.gz'
744    urls = ('http://google-visualization-python.googlecode.com/files/%s' % (
745        url_filename),)
746    hex_sum = 'cd9a0fb4ca5c4f86c0d85756f501fd54ccf492d2'
747
748    _build_and_install = ExternalPackage._build_and_install_from_package
749    _build_and_install_current_dir = (
750                        ExternalPackage._build_and_install_current_dir_noegg)
751