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