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