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