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