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