xref: /aosp_15_r20/external/autotest/utils/external_packages.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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