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